Статьи

Основные данные и Swift: миграции

В предыдущих статьях этой серии мы столкнулись с досадной проблемой, которую нам нужно решить. Всякий раз, когда мы изменяем модель данных приложения Core Data, постоянное хранилище становится несовместимым с моделью данных. В результате происходит сбой при запуске, что делает приложение непригодным для использования, серьезная проблема, если это происходит с приложением в App Store.

Наше приложение аварийно завершает работу, потому что мы вызываем abort если добавление постоянного хранилища в координатор постоянного хранилища не удается. Чтобы было ясно, функция abort заставляет приложение немедленно завершиться.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
do {
    // Add Persistent Store to Persistent Store Coordinator
    try persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: URLPersistentStore, options: nil)
     
} catch {
    // Populate Error
    var userInfo = [String: AnyObject]()
    userInfo[NSLocalizedDescriptionKey] = «There was an error creating or loading the application’s saved data.»
    userInfo[NSLocalizedFailureReasonErrorKey] = «There was an error creating or loading the application’s saved data.»
     
    userInfo[NSUnderlyingErrorKey] = error as NSError
    let wrappedError = NSError(domain: «com.tutsplus.Done», code: 1001, userInfo: userInfo)
     
    NSLog(«Unresolved error \(wrappedError), \(wrappedError.userInfo)»)
     
    abort()
}

Тем не менее, нет необходимости останавливать наше приложение, не говоря уже о его сбое. Если Core Data сообщает нам, что модель данных и постоянное хранилище несовместимы, мы должны решить эту проблему.

В этой статье мы обсудим два варианта выхода из такой ситуации: миграция постоянного хранилища и создание нового постоянного хранилища, совместимого с измененной моделью данных.

Позвольте мне сначала прояснить проблему, которую мы пытаемся решить. Загрузите пример проекта, который мы создали в предыдущей статье, и запустите его в симуляторе. Приложение должно работать и нормально работать.

Откройте Done.xcdatamodeld и добавьте атрибут updatedAt типа Date в сущность Item . Запустите приложение еще раз и обратите внимание, как происходит сбой приложения, как только оно запущено. К счастью, Core Data дает нам подсказку о том, что пошло не так. Посмотрите на вывод в консоли XCode.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Done[897:14527] CoreData: error: -addPersistentStoreWithType:SQLite configuration:(null) URL:file:///Users/Bart/Library/Developer/CoreSimulator/Devices/A263775B-4D73-48C8-BD79-825E0BED5128/data/Containers/Data/Application/E46663CA-79AF-4645-AF78-0A17236943E1/Documents/Done.sqlite options:(null) … returned error Error Domain=NSCocoaErrorDomain Code=134100 «(null)» UserInfo={metadata={
    NSPersistenceFrameworkVersion = 640;
    NSStoreModelVersionHashes = {
        Item = <4c880226 3219fc66 283b28c5 54f026dc 7f95af5f c19fb76e 255a26a7 2a2a79f5>;
    };
    NSStoreModelVersionHashesVersion = 3;
    NSStoreModelVersionIdentifiers = (
        «»
    );
    NSStoreType = SQLite;
    NSStoreUUID = «F0F98261-4F60-451A-9606-91E1F60425B9»;
    «_NSAutoVacuumLevel» = 2;
}, reason=The model used to open the store is incompatible with the one used to create the store} with userInfo dictionary {
    metadata = {
        NSPersistenceFrameworkVersion = 640;
        NSStoreModelVersionHashes = {
            Item = <4c880226 3219fc66 283b28c5 54f026dc 7f95af5f c19fb76e 255a26a7 2a2a79f5>;
        };
        NSStoreModelVersionHashesVersion = 3;
        NSStoreModelVersionIdentifiers = (
            «»
        );
        NSStoreType = SQLite;
        NSStoreUUID = «F0F98261-4F60-451A-9606-91E1F60425B9»;
        «_NSAutoVacuumLevel» = 2;
    };
    reason = «The model used to open the store is incompatible with the one used to create the store»;
}

Ближе к концу Core Data сообщает нам, что модель данных, которая использовалась для открытия постоянного хранилища, несовместима с моделью данных, которая использовалась для создания постоянного хранилища. Подождите. Какая?

Когда мы впервые запустили приложение, Core Data создала базу данных SQLite на основе модели данных. Однако, поскольку мы изменили модель данных, добавив атрибут к сущности Item , updatedAt , Core Data больше не понимает, как следует хранить записи Item в базе данных SQLite. Другими словами, измененная модель данных больше не совместима с постоянным хранилищем, базой данных SQLite, созданной ранее.

К счастью для нас, несколько умных инженеров в Apple создали решение для безопасной модификации модели данных без проблем с совместимостью. Чтобы решить проблему, с которой мы сталкиваемся, нам нужно найти способ сообщить Core Data, как одна версия модели данных связана с другой версией. Это верно, управление версиями модели данных является частью решения.

С помощью этой информации Core Data может понять, как необходимо обновить постоянное хранилище, чтобы оно было совместимо с измененной моделью данных, то есть с новой версией модели данных. Другими словами, нам нужно передать Core Data необходимую информацию для переноса постоянного хранилища из одной версии модели данных в другую.

Существует два типа миграций: легкая и тяжелая. Слова « легкий» и « тяжелый» довольно описательны, но важно понимать, как Core Data обрабатывает каждый тип миграции.

Облегченные миграции требуют от разработчика очень мало работы. Я настоятельно рекомендую вам выбирать легкую миграцию вместо тяжелой, когда это возможно. Стоимость легкой миграции существенно ниже стоимости тяжелой миграции.

Конечно, обратной стороной легких миграций является то, что они менее мощные, чем тяжелые миграции. Изменения, которые вы можете внести в модель данных с облегченными миграциями, ограничены. Например, облегченная миграция позволяет добавлять или переименовывать атрибуты и сущности, но нельзя изменить тип атрибута или отношения между существующими сущностями.

Облегченные миграции идеально подходят для расширения модели данных, добавления атрибутов и сущностей. Если вы планируете изменить отношения или типы атрибутов, то вас ждет дикая поездка с тяжелыми миграциями.

Тяжелые миграции немного сложнее. Позвольте мне перефразировать это. Тяжелые миграции — это боль в шее, и вы должны стараться избегать их, если это возможно. Тяжелые миграции мощны, но эта сила обходится дорого. Тяжелые миграции требуют большой работы и тестирования, чтобы убедиться, что миграция завершена успешно и, что более важно, без потери данных.

Мы вступаем в мир тяжелых миграций, если вносим изменения, которые Core Data не может автоматически определить для нас, сравнивая версии модели данных. Базовым данным тогда потребуется модель отображения, чтобы понять, как версии модели данных связаны друг с другом. Поскольку тяжелые миграции — сложная тема, мы не будем ее освещать в этой серии.

Если вы работали с Ruby on Rails или любым другим фреймворком, который поддерживает миграции, то миграция Core Data будет иметь для вас большой смысл. Идея простая, но мощная. Базовые данные позволяют нам создавать версии модели данных, и это позволяет нам безопасно изменять модель данных. Core Data проверяет версионную модель данных, чтобы понять, как постоянное хранилище связано с моделью данных. Рассматривая версионную модель данных, она также знает, нужно ли переносить постоянное хранилище, прежде чем его можно будет использовать с текущей версией модели данных.

Версионирование и миграция идут рука об руку. Если вы хотите понять, как работают миграции, вам сначала нужно понять, как создать версию модели данных Core Data. Давайте вернемся к приложению, которое мы создали в предыдущей статье . Как мы видели ранее, добавление атрибута updatedAt к сущности Item приводит к несовместимости постоянного хранилища с измененной моделью данных. Теперь мы понимаем причину этого.

Давайте начнем с чистого листа, открыв Done.xcdatamodeld и удалив атрибут updatedAt из сущности Item . Пришло время создать новую версию модели данных.

Выбрав модель данных, выберите « Добавить версию модели …» в меню « Редактор» . Xcode попросит вас назвать новую версию модели данных и, что более важно, на какой версии должна базироваться новая версия. Чтобы Core Data могла перенести постоянное хранилище для нас, важно, чтобы вы выбрали предыдущую версию модели данных. В этом примере у нас есть только один выбор.

Добавление версии модели

Результатом этого действия является то, что теперь мы видим три файла модели данных в Project Navigator . Существует одна модель данных верхнего уровня с расширением .xcdatamodeld и два дочерних элемента с расширением .xcdatamodel .

Версии модели данных

Вы можете увидеть файл .xcdatamodeld как пакет для версий модели данных, где каждая версия представлена ​​файлом .xcdatamodel . Вы можете убедиться в этом, щелкнув правой кнопкой мыши файл .xcdatamodeld и выбрав Показать в Finder . Это приведет вас к модели данных в проекте Xcode. Вы должны увидеть две версии модели данных: Done.xcdatamodel и Done 2 .xcdatamodel .

Вы заметили в Project Navigator, что одна из версий имеет зеленую галочку? Этот флажок указывает текущую версию модели Done.xcdatamodel в этом примере. Другими словами, хотя мы создали новую версию модели данных, она еще не используется нашим приложением. Прежде чем мы изменим это, нам нужно сообщить Core Data, что следует делать с версионной моделью данных.

Нам нужно сообщить Core Data, как перенести постоянное хранилище для модели данных. Мы делаем это, когда добавляем постоянное хранилище в координатор постоянного хранилища в AppDelegate.swift . В реализации свойства persistentStoreCoordinator мы создаем координатор постоянного хранилища и добавляем в него постоянное хранилище, вызывая addPersistentStoreWithType(_:configuration:URL:options:) . Это должно казаться знакомым к настоящему времени.

Четвертый параметр этого метода — это словарь опций, который в настоящее время равен nil . Этот словарь опций включает инструкции для Core Data. Это дает нам возможность сообщить координатору постоянного хранилища, как он должен перенести постоянное хранилище, которое мы хотим добавить в него.

Взгляните на следующий фрагмент кода, в котором мы передаем словарь опций с двумя парами ключ-значение.

1
2
3
4
5
// Declare Options
let options = [ NSMigratePersistentStoresAutomaticallyOption : true, NSInferMappingModelAutomaticallyOption : true ]
 
// Add Persistent Store to Persistent Store Coordinator
try persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: URLPersistentStore, options: options)

Первый ключ, NSMigratePersistentStoresAutomaticallyOption , сообщает Core Data, что мы хотели бы, чтобы он попытался перенести постоянное хранилище для нас. Второй ключ, NSInferMappingModelAutomaticallyOption , указывает Core Data на вывод модели сопоставления для миграции. Это именно то, что мы хотим. Это должно работать без проблем, пока мы имеем дело с легкими миграциями.

С этим изменением мы готовы перенести модель данных в новую версию, которую мы создали несколько минут назад. Начните с выбора новой версии Done 2.xcdatamodel и добавьте новый атрибут updatedAt типа Date в сущность Item .

Нам также нужно пометить новую версию модели данных как версию, используемую Core Data. Выбрать Done.xcdatamodeld в Project Navigator и откройте инспектор файлов справа. В разделе Версия модели установите Текущий на Готово 2 .

Обновление версии модели

В Навигаторе проекта , Done 2.xcdatamodel теперь должен иметь зеленую галочку вместо Done.xcdatamodel .

Управление версиями модели данных

С этим изменением вы можете безопасно собрать и запустить приложение. Если вы выполнили вышеуказанные шаги, Core Data должна автоматически перенести постоянное хранилище для вас, выведя модель сопоставления на основе версионной модели данных.

Обратите внимание, что есть несколько предостережений, о которых вы должны знать. Если вы столкнулись с аварией, значит, вы сделали что-то не так. Например, если вы установили версию модели данных на Done 2.xcdatamodel , запустите приложение, а затем внесете изменения в Done 2.xcdatamodel , то вы, скорее всего, столкнетесь с падением из-за несовместимости постоянного хранилища с модель данных. Облегченные миграции являются относительно мощными и простыми в реализации, но это не значит, что вы можете изменить модель данных в любое время.

Уровень данных программного проекта требует заботы, внимания и подготовки. Миграции велики, но их следует использовать с осторожностью. Сбои не являются проблемой во время разработки, но они катастрофичны в производстве. В следующем разделе мы подробнее рассмотрим, что это значит и как предотвратить сбои из-за проблемной миграции.

Я никогда не сталкивался с ситуацией, когда требуется abort вызова в производственной среде, и мне больно, когда я просматриваю проект, в котором используется реализация Apple по умолчанию для настройки стека Core Data, в которой abort вызывается, когда добавление постоянного хранилища не удается.

Предотвращение abort не так сложно, но требует нескольких строк кода и информирования пользователя о том, что пошло не так, если что-то пойдет не так. Разработчики — всего лишь люди, и мы все совершаем ошибки, но это не значит, что вы должны бросить полотенце и вызвать abort .

Начните с открытия AppDelegate.swift и удалите строку, в которой мы вызываем abort . Это первый шаг к счастливому пользователю.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
do {
    // Declare Options
    let options = [ NSMigratePersistentStoresAutomaticallyOption : true, NSInferMappingModelAutomaticallyOption : true ]
     
    // Add Persistent Store to Persistent Store Coordinator
    try persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: URLPersistentStore, options: options)
     
} catch {
    // Populate Error
    var userInfo = [String: AnyObject]()
    userInfo[NSLocalizedDescriptionKey] = «There was an error creating or loading the application’s saved data.»
    userInfo[NSLocalizedFailureReasonErrorKey] = «There was an error creating or loading the application’s saved data.»
     
    userInfo[NSUnderlyingErrorKey] = error as NSError
    let wrappedError = NSError(domain: «com.tutsplus.Done», code: 1001, userInfo: userInfo)
     
    NSLog(«Unresolved error \(wrappedError), \(wrappedError.userInfo)»)
}

Если Базовые Данные обнаруживают, что постоянное хранилище несовместимо с моделью данных, мы сначала перемещаем несовместимое хранилище в безопасное место. Мы делаем это, чтобы убедиться, что данные пользователя не потеряны. Даже если модель данных несовместима с постоянным хранилищем, вы сможете восстановить данные из нее. Взгляните на обновленную реализацию свойства persistentStoreCoordinator в AppDelegate.swift .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
    // Initialize Persistent Store Coordinator
    let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
     
    // URL Persistent Store
    let URLPersistentStore = self.applicationStoresDirectory().URLByAppendingPathComponent(«Done.sqlite»)
     
    do {
        // Declare Options
        let options = [ NSMigratePersistentStoresAutomaticallyOption : true, NSInferMappingModelAutomaticallyOption : true ]
         
        // Add Persistent Store to Persistent Store Coordinator
        try persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: URLPersistentStore, options: options)
         
    } catch {
        let fm = NSFileManager.defaultManager()
         
        if fm.fileExistsAtPath(URLPersistentStore.path!) {
            let nameIncompatibleStore = self.nameForIncompatibleStore()
            let URLCorruptPersistentStore = self.applicationIncompatibleStoresDirectory().URLByAppendingPathComponent(nameIncompatibleStore)
             
            do {
                // Move Incompatible Store
                try fm.moveItemAtURL(URLPersistentStore, toURL: URLCorruptPersistentStore)
                 
            } catch {
                let moveError = error as NSError
                print(«\(moveError), \(moveError.userInfo)»)
            }
        }
    }
     
    return persistentStoreCoordinator
}()

Обратите внимание, что я изменил значение URLPersistentStore , местоположение постоянного хранилища. Он указывает на каталог в каталоге Documents в песочнице приложения. Реализация applicationStoresDirectory() , вспомогательного метода, проста, как вы можете видеть ниже. Это, конечно, более многословно, чем в Objective-C. Также обратите внимание, что я принудительно распаковываю результат path() константы URL , потому что мы можем с уверенностью предположить, что в изолированной программной среде приложения есть каталог поддержки приложения, как на iOS, так и на OS X.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private func applicationStoresDirectory() -> NSURL {
    let fm = NSFileManager.defaultManager()
     
    // Fetch Application Support Directory
    let URLs = fm.URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask)
    let applicationSupportDirectory = URLs[(URLs.count — 1)]
     
    // Create Application Stores Directory
    let URL = applicationSupportDirectory.URLByAppendingPathComponent(«Stores»)
     
    if !fm.fileExistsAtPath(URL.path!) {
        do {
            // Create Directory for Stores
            try fm.createDirectoryAtURL(URL, withIntermediateDirectories: true, attributes: nil)
             
        } catch {
            let createError = error as NSError
            print(«\(createError), \(createError.userInfo)»)
        }
    }
     
    return URL
}

Если координатор постоянного хранилища не может добавить существующее постоянное хранилище в URLPersistentStore , мы перемещаем постоянное хранилище в отдельный каталог. Для этого мы используем еще два вспомогательных метода: applicationIncompatibleStoresDirectory() и nameForIncompatibleStore() . Реализация applicationIncompatibleStoresDirectory() похожа на реализацию applicationStoresDirectory() .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
private func applicationIncompatibleStoresDirectory() -> NSURL {
    let fm = NSFileManager.defaultManager()
     
    // Create Application Incompatible Stores Directory
    let URL = applicationStoresDirectory().URLByAppendingPathComponent(«Incompatible»)
     
    if !fm.fileExistsAtPath(URL.path!) {
        do {
            // Create Directory for Stores
            try fm.createDirectoryAtURL(URL, withIntermediateDirectories: true, attributes: nil)
             
        } catch {
            let createError = error as NSError
            print(«\(createError), \(createError.userInfo)»)
        }
    }
     
    return URL
}

В nameForIncompatibleStore() мы генерируем имя для несовместимого хранилища на основе текущей даты и времени, чтобы избежать конфликтов имен.

01
02
03
04
05
06
07
08
09
10
private func nameForIncompatibleStore() -> String {
    // Initialize Date Formatter
    let dateFormatter = NSDateFormatter()
     
    // Configure Date Formatter
    dateFormatter.formatterBehavior = .Behavior10_4
    dateFormatter.dateFormat = «yyyy-MM-dd-HH-mm-ss»
     
    return «\(dateFormatter.stringFromDate(NSDate())).sqlite»
}

Пришло время создать новое постоянное хранилище, чтобы завершить настройку стека основных данных. Следующие несколько строк должны выглядеть очень знакомыми.

01
02
03
04
05
06
07
08
09
10
11
do {
    // Declare Options
    let options = [ NSMigratePersistentStoresAutomaticallyOption : true, NSInferMappingModelAutomaticallyOption : true ]
     
    // Add Persistent Store to Persistent Store Coordinator
    try persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: URLPersistentStore, options: options)
     
} catch {
    let storeError = error as NSError
    print(«\(storeError), \(storeError.userInfo)»)
}

Если Core Data не может создать новое постоянное хранилище, возникают более серьезные проблемы, не связанные с несовместимостью модели данных с постоянным хранилищем. Если вы столкнулись с этой проблемой, то дважды проверьте значение URLPersistentStore .

Этот шаг, вероятно, является наиболее важным с точки зрения создания удобного для пользователя приложения. Потеря данных пользователя — это одно, но делать вид, что ничего не произошло, нехорошо. Как бы вы себя чувствовали, если бы авиакомпания потеряла ваш багаж, делая вид, что ничего не произошло.

1
2
3
4
5
6
let storeError = error as NSError
print(«\(storeError), \(storeError.userInfo)»)
 
// Update User Defaults
let userDefaults = NSUserDefaults.standardUserDefaults()
userDefaults.setBool(true, forKey: «didDetectIncompatibleStore»)

Если Core Data не удалось перенести постоянное хранилище с использованием модели данных, мы запомним это, установив пару ключ-значение в базе данных пользователя по умолчанию приложения. Мы ищем эту пару ключ-значение в viewDidLoad() класса ViewController .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// MARK: —
// MARK: View Life Cycle
override func viewDidLoad() {
    super.viewDidLoad()
     
    do {
        try self.fetchedResultsController.performFetch()
    } catch {
        let fetchError = error as NSError
        print(«\(fetchError), \(fetchError.userInfo)»)
    }
     
    let userDefaults = NSUserDefaults.standardUserDefaults()
    let didDetectIncompatibleStore = userDefaults.boolForKey(«didDetectIncompatibleStore»)
     
    if didDetectIncompatibleStore {
        // Show Alert
        let applicationName = NSBundle.mainBundle().objectForInfoDictionaryKey(«CFBundleDisplayName»)
        let message = «A serious application error occurred while \(applicationName) tried to read your data. Please contact support for help.»
         
        self.showAlertWithTitle(«Warning», message: message, cancelButtonTitle: «OK»)
    }
}

Реализация showAlertWithTitle(_:message:cancelButtonTitle:) похожа на ту, что мы видели в AddToDoViewController . Обратите внимание, что мы удаляем пару ключ-значение, когда пользователь нажимает кнопку оповещения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
// MARK: —
// MARK: Helper Methods
private func showAlertWithTitle(title: String, message: String, cancelButtonTitle: String) {
    // Initialize Alert Controller
    let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert)
     
    // Configure Alert Controller
    alertController.addAction(UIAlertAction(title: cancelButtonTitle, style: .Default, handler: { (_) -> Void in
        let userDefaults = NSUserDefaults.standardUserDefaults()
        userDefaults.removeObjectForKey(«didDetectIncompatibleStore»)
    }))
     
    // Present Alert Controller
    presentViewController(alertController, animated: true, completion: nil)
}

Мы показываем пользователю предупреждение, но это хорошая идея сделать еще несколько шагов. Например, вы можете попросить их связаться со службой поддержки и даже реализовать функцию, которая позволяет отправлять вам поврежденный магазин. Последнее очень полезно для устранения проблемы.

Сохранение данных является важным аспектом большинства приложений. Поэтому важно правильно протестировать то, что мы реализовали в этой статье. Чтобы проверить нашу стратегию восстановления, запустите приложение в симуляторе и дважды проверьте, что постоянное хранилище было успешно создано в каталоге поддержки приложений в подкаталоге Stores .

Добавьте новый атрибут к сущности Item в Done 2.xcdatamodel и запустите приложение еще раз. Поскольку существующее постоянное хранилище теперь несовместимо с моделью данных, несовместимое постоянное хранилище перемещается в подкаталог Incompatible, и создается новое постоянное хранилище. Вы также должны увидеть предупреждение, информирующее пользователя о проблеме.

Информирование пользователя

Миграции являются важной темой, если вы планируете широко использовать базовые данные. Миграции позволяют безопасно изменять модель данных вашего приложения и, в случае облегченных миграций, без особых накладных расходов.

В следующей статье мы сосредоточимся на NSManagedObject подкласса NSManagedObject . Если у проекта Core Data есть какая-либо сложность, тогда подклассы NSManagedObject — это путь.