Статьи

Основные данные с нуля: Миграции

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

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

1
2
3
4
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
    NSLog(@»Unresolved error %@, %@», error, [error userInfo]);
    abort();
}

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

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
Error Domain=NSCocoaErrorDomain Code=134100 «The operation couldn’t be completed. (Cocoa error 134100.)»
UserInfo=0x7c57e980 {
metadata={
    NSPersistenceFrameworkVersion = 513;
    NSStoreModelVersionHashes = {
        TSPItem = <ce1c6693 4229b043 7cfe7324 4718b0c4 81c9af4d 12e71373 3288b42e 7de7ac62>;
    };
    NSStoreModelVersionHashesVersion = 3;
    NSStoreModelVersionIdentifiers = (
        «»
    );
    NSStoreType = SQLite;
    NSStoreUUID = «C6212594-143E-4A26-9990-7FE5FD8B7336»;
    «_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 на основе модели данных. Однако, поскольку мы изменили модель данных, добавив атрибут к сущности TSPItem , updatedAt , Core Data больше не понимает, как следует хранить записи TSPItem в базе данных SQLite. Другими словами, измененная модель данных больше не совместима с постоянным хранилищем, созданным ранее.

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

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

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

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

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

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

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

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

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

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

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

Выбрав модель данных, выберите « Добавить версию модели …» в меню « Редактор» . 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, как перенести постоянное хранилище для модели данных. Мы делаем это в методе persistentStoreCoordinator в TSPAppDelegate.m . В методе persistentStoreCoordinator мы создаем координатор постоянного хранилища и добавляем в него постоянное хранилище, вызывая addPersistentStoreWithType:configuration:URL:options:error: В этом нет ничего нового.

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
— (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    if (_persistentStoreCoordinator) {
        return _persistentStoreCoordinator;
    }
     
    NSURL *applicationDocumentsDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
    NSURL *storeURL = [applicationDocumentsDirectory URLByAppendingPathComponent:@»Done.sqlite»];
     
    NSError *error = nil;
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
     
    NSDictionary *options = @{ NSMigratePersistentStoresAutomaticallyOption : @(YES),
                               NSInferMappingModelAutomaticallyOption : @(YES) };
     
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) {
        NSLog(@»Unresolved error %@, %@», error, [error userInfo]);
        abort();
    }
     
    return _persistentStoreCoordinator;
}

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

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

Нам также нужно пометить новую версию модели данных как версию, используемую 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 не так сложно, но требует нескольких строк кода и информирования пользователя о том, что пошло не так, если что-то пойдет не так. Разработчики — только люди, и мы все делаем ошибки.

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

1
2
3
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) {
    NSLog(@»Unresolved error %@, %@», error, [error userInfo]);
}

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

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
— (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    if (_persistentStoreCoordinator) {
        return _persistentStoreCoordinator;
    }
     
    NSURL *storeURL = [[self applicationStoresDirectory] URLByAppendingPathComponent:@»Store.sqlite»];
     
    NSError *error = nil;
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
     
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
        NSFileManager *fm = [NSFileManager defaultManager];
         
        // Move Incompatible Store
        if ([fm fileExistsAtPath:[storeURL path]]) {
            NSURL *corruptURL = [[self applicationIncompatibleStoresDirectory] URLByAppendingPathComponent:[self nameForIncompatibleStore]];
             
            // Move Corrupt Store
            NSError *errorMoveStore = nil;
            [fm moveItemAtURL:storeURL toURL:corruptURL error:&errorMoveStore];
             
            if (errorMoveStore) {
                NSLog(@»Unable to move corrupt store.»);
            }
        }
    }
     
    return _persistentStoreCoordinator;
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
— (NSURL *)applicationStoresDirectory {
    NSFileManager *fm = [NSFileManager defaultManager];
    NSURL *applicationApplicationSupportDirectory = [[fm URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask] lastObject];
    NSURL *URL = [applicationApplicationSupportDirectory URLByAppendingPathComponent:@»Stores»];
     
    if (![fm fileExistsAtPath:[URL path]]) {
        NSError *error = nil;
        [fm createDirectoryAtURL:URL withIntermediateDirectories:YES attributes:nil error:&error];
         
        if (error) {
            NSLog(@»Unable to create directory for data stores.»);
             
            return nil;
        }
    }
     
    return URL;
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
— (NSURL *)applicationIncompatibleStoresDirectory {
    NSFileManager *fm = [NSFileManager defaultManager];
    NSURL *URL = [[self applicationStoresDirectory] URLByAppendingPathComponent:@»Incompatible»];
     
    if (![fm fileExistsAtPath:[URL path]]) {
        NSError *error = nil;
        [fm createDirectoryAtURL:URL withIntermediateDirectories:YES attributes:nil error:&error];
         
        if (error) {
            NSLog(@»Unable to create directory for corrupt data stores.»);
             
            return nil;
        }
    }
     
    return URL;
}

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

01
02
03
04
05
06
07
08
09
10
— (NSString *)nameForIncompatibleStore {
    // Initialize Date Formatter
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
     
    // Configure Date Formatter
    [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
    [dateFormatter setDateFormat:@»yyyy-MM-dd-HH-mm-ss»];
     
    return [NSString stringWithFormat:@»%@.sqlite», [dateFormatter stringFromDate:[NSDate date]]];
}

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

1
2
3
4
NSError *errorAddingStore = nil;
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&errorAddingStore]) {
    NSLog(@»Unable to create persistent store after recovery. %@, %@», errorAddingStore, errorAddingStore.localizedDescription);
}

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

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

1
2
3
4
5
6
// Show Alert View
NSString *title = @»Warning»;
NSString *applicationName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@»CFBundleDisplayName»];
NSString *message = [NSString stringWithFormat:@»A serious application error occurred while %@ tried to read your data. Please contact support for help.», applicationName];
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:title message:message delegate:nil cancelButtonTitle:@»OK» otherButtonTitles:nil];
[alertView show];

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

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

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

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

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