Реалізація міграції схеми бази даних (Core Data Migration) в iOS-додатках
loadPersistentStores повернув помилку NSMigrationError — і додаток не запустився. Це трапляється, коли розробник додав новий атрибут у .xcdatamodeld, забув створити нову версію моделі, і додаток виявив невідповідність між кодом і сховищем. Для користувача це крах при запуску. Для команди — терміновий фікс о 2 ночі.
Версіонування моделі даних
Core Data зберігає всі версії моделі у xcdatamodeld-пакеті (це папка з файлами *.xcdatamodel всередину). Активна версія вказується у .xccurrentversion. При зміні схеми потрібно:
- У Xcode: Editor → Add Model Version
- Встановити нову версію як Current Version
- Описати міграцію
Ніколи не редагуйте існуючу версію моделі, якщо додаток вже у production — це гарантований крах для всіх користувачів.
Lightweight Migration — коли працює
Легкавісна міграція (NSInferMappingModelAutomatically) працює автоматично для:
- Додавання нового атрибута з
optional = trueабо зdefaultValue - Видалення атрибута
- Переименування entity або атрибута при наявності
Renaming Identifier
Включається однією строкою:
let options: [String: Any] = [
NSMigratePersistentStoresAutomaticallyOption: true,
NSInferMappingModelAutomaticallyOption: true
]
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options)
Якщо Renaming Identifier у .xcdatamodeld встановлений правильно — Core Data сам побудує маппінг між версіями. Для NSPersistentContainer:
container.persistentStoreDescriptions.first?.shouldMigrateStoreAutomatically = true
container.persistentStoreDescriptions.first?.shouldInferMappingModelAutomatically = true
Heavyweight Migration — коли автоматика не справляється
Якщо тип атрибута змінився, додано ненульове обов'язкове поле без default, або потрібна трансформація даних при міграції — потрібна кастомна NSEntityMigrationPolicy.
class TransactionMigrationPolicy: NSEntityMigrationPolicy {
override func createDestinationInstances(
forSource sourceInstance: NSManagedObject,
in mapping: NSEntityMapping,
manager: NSMigrationManager
) throws {
let destination = NSEntityDescription.insertNewObject(
forEntityName: mapping.destinationEntityName!,
into: manager.destinationContext
)
// Копіюємо атрибути
destination.setValue(sourceInstance.value(forKey: "amount"), forKey: "amount")
// Трансформуємо: старий String → новий enum Int
let categoryString = sourceInstance.value(forKey: "category") as? String ?? ""
destination.setValue(CategoryMapper.intValue(for: categoryString), forKey: "categoryRaw")
manager.associate(sourceInstance: sourceInstance, withDestinationInstance: destination, for: mapping)
}
}
NSEntityMigrationPolicy вказується у MappingModel.xcmappingmodel — файл, який створюється через Xcode: New File → Mapping Model. У ньому описується відповідність entity старої версії → entity нової версії та який Policy клас використовувати.
Прогресивна міграція через кілька версій
Якщо користувач не оновлював додаток із v1 на v5 — Core Data не вміє автоматично будувати ланцюги міграцій. Потрібен менеджер:
class MigrationManager {
func migrateStore(at storeURL: URL) throws {
var currentURL = storeURL
while true {
guard let sourceModel = NSManagedObjectModel.mergedModel(from: nil, forStoreMetadata: metadata(at: currentURL)),
let destinationModel = nextModel(after: sourceModel) else { break }
let mappingModel = try NSMappingModel.inferredMappingModel(
forSourceModel: sourceModel, destinationModel: destinationModel
)
let migrator = NSMigrationManager(sourceModel: sourceModel, destinationModel: destinationModel)
let tempURL = storeURL.appendingPathExtension("migration")
try migrator.migrateStore(from: currentURL, type: .sqlite, to: tempURL, type: .sqlite, mapping: mappingModel)
try FileManager.default.removeItem(at: currentURL)
try FileManager.default.moveItem(at: tempURL, to: storeURL)
}
}
}
Міграція виконується до інініціалізації NSPersistentContainer — на splash screen з індикатором прогресу.
Резервна копія перед міграцією
Завжди робимо бекап перед heavyweight migration:
let backupURL = storeURL.deletingLastPathComponent()
.appendingPathComponent("backup_\(Date().timeIntervalSince1970).sqlite")
try FileManager.default.copyItem(at: storeURL, to: backupURL)
Якщо міграція упала — відновлюємо бекап. Критично для даних, які неможливо відновити.
Типічні помилки
- Редагування поточної версії моделі замість створення нової —
Model version checksums don't match - Важка міграція на main thread — UI зависає на кілька секунд при великій базі
- Не тестувати міграцію з реальним
.sqliteфайлом — помилки виявляються тільки на пристрої користувача
Обсяг роботи
- Аудит поточної моделі та історії версій
- Створення нових версій
.xcdatamodeld - Lightweight або heavyweight міграція залежно від змін
- Кастомна
NSEntityMigrationPolicyпри трансформації даних - Прогресивна міграція через кілька версій
- Резервне копіювання перед міграцією
Строки
Lightweight міграція (додавання атрибутів): 0,5 дня. Heavyweight з кастомними policy та прогресивними переходами між кількома версіями: 2–3 дні.







