Реализация миграции схемы базы данных (Core Data Migration) в iOS-приложении
loadPersistentStores вернул ошибку NSMigrationError — и приложение не запустилось. Это случается, когда разработчик добавил новый атрибут в .xcdatamodeld, забыл создать новую версию модели, и приложение обнаружило несоответствие между кодом и хранилищем. Для пользователя это крэш при запуске. Для команды — срочный фикс в 2 часа ночи.
Версионирование модели данных
Core Data хранит все версии модели в xcdatamodeld-пакете (это папка с файлами *.xcdatamodel внутри). Активная версия указывается в .xccurrentversion. При изменении схемы нужно:
- В Xcode: Editor → Add Model Version
- Установить новую версию как Current Version
- Описать миграцию
Никогда не редактировать существующую версию модели, если приложение уже в продакшене — это гарантированный крэш у всех пользователей.
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 дня.







