Core Data: Часть 2. Lightweight миграции

Если вы нашли опечатку в тексте, выделите ее и нажмите CTRL + ENTER.

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

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

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

Заметка:

Этот туториал подразумевает ваше базовое знакомство с Core Data и Swift. Если вы не знакомы с Core Data, то вам лучше начать с "Core Data: Часть 1". Если вы не знакомы со Swift, то вам стоит прочитать "Swift Часть 1: Быстрый старт".

Когда нам нужна миграция?

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

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

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

Процесс миграции

Когда вы инициализируете стек Core Data, то одним из этапов по инициализации является добавление координатора хранилища. Когда вы попадаете на этот этап, Core Data выполняет еще кое-какие действия перед добавлением этого хранилища координатору.

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

Заметка:

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

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

Миграция происходит в три этапа:

  1. Сначала Core Data копирует все объекты одного хранилища в следующее.
  2. Далее, Core Data соединяет и соотносит все объекты, исходя из взаимоотношений.
  3. Проверяет соответствие данных в конечной модели, отключает проверку данных в конечной модели на этапе копирования.

Вы можете спросить: "Но если что-то пойдет не так, что случится с хранилищем исходных данных?". С ним ничего не происходит до тех пор, пока Core Data не подтвердит успешность завершения миграции, после чего исходное хранилище удаляется.

Типы миграций

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

Простые миграции

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

Ручные миграции

Manual миграции (ручные) включают в себя немного больше работы с вашей стороны. Вам нужно определить соответствие набора старых данных, в соответствии с новым набором. В качестве бонуса у вас появляется возможность более явной настройки файла модели соответствия. Настройка модели соответствия в Xcode аналогична настройке модели данных с инструментами GUI и некоторой автоматизацией.

Custom Manual миграции

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

Полностью ручные миграции

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

Поехали!

В этом туториале вы сосредоточитесь на простых миграциях, потому что они являются самым простым и распространенным случаем. Ну что, поехали? Поехали!

Сначала скачайте стартовый проект этого туториала.

Запустите приложение на симуляторе iPhone. Вы увидите пустой список заметок:

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

Откройте файл UnCloudNotesDatamodel.xcdatamodeld для того, чтобы открыть редактор моделей в Xcode. Наша модель данных представляет из себя единственную сущность Note с несколькими атрибутами.

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

Простая миграция (lightweight)

В открытом редакторе выберите в меню Add Model Version. Назовите новую версию UnCloudNotesDataModel v2, а в поле "Based on Model" UnCloudNotesDataModel. Xcode создаст копию вашей модели данных.

Заметка:

Вы можете дать этому файлу любое имя, какое только захотите. Последовательное обозначение v2, v3, v4 помогает отследить последовательное изменение версий.

Этот шаг создаст вторую версию вашей модели данных, но вам все еще нужно сообщить Xcode, чтобы он использовал новую версию в качестве рабочей модели. В инспекторе файла (File Inspector), справа вверху, найдите секцию Model Version. Выберите в поле "current" UnCloudNotesDataModel v2:

После того, как вы сделали это, обратите внимание, что у вас появилась зеленая отметка напротив новой версии модели данных:

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

Убедитесь в том, что вы выбрали вторую версию модели, добавьте атрибут image и установите его тип как Tranformable.

Так как атрибут будет содержать бинарные биты изображения, то вы будете использовать NSValueTransformer для трансформации бинарных битов в UIImage, и обратно. Именной такой трансформер был предоставлен как ImageTransformer. В поле Value Transformer Name, в инспекторе модели данных введите UnCloudNotes.ImageTransformer.

Новая модель теперь готова для написания некоторого кода! Откройте Note.swift и добавьте свойство для соответствия новому атрибуту:

@NSManaged var image: UIImage?

Запустите приложение и вы увидите, что ваши заметки исчезли! В консоли Xcode появится ошибка, которая относится к объекту CoreDataStack:

context: <NSManagedObjectContext: 0x7a96c8f0> modelName: UnCloudNotesDataModelmodel: [Note: <c393d6c8 d5e65f5a bb5e4394 eb6ce54c a99e724d bdb64072 ed2e99dd 99e77ba0>] coordinator: <NSPersistentStoreCoordinator: 0x7a973310> storeURL: file:///Users/YOURNAME/Library/Developer/CoreSimulator/Devices/A24A4E68-D616-4F63-8946-652164EE5E53/data/Containers/Data/Application/9921B2DD-D0FD-4330-90F9-A2F44CC9899A/Library/Application%20Support/UnCloudNotes.sqlite store: nil

Файл хранилища все еще активен (storeURL в логе выше), но так как теперь он не совместим с версией модели v2, Core Data не может соединить его с постоянным хранилищем. По этому store имеет значение nil. Core Data может автоматически обновлять ваше хранилище, если вы добавили новое свойство как у нас в примере. Это и называется простая миграция (lightweight migration).

Откройте CoreDataStack.swift и добавьте свойство классу:

var options: NSDictionary?

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

Следующим шагом обновите инициализатор:

init(modelName: String, storeName: String,
 options: NSDictionary? = nil) {
  self.modelName = modelName
  self.storeName = storeName
  self.options = options
}

При установке дефолтного значения options в nil, старый метод инициализации так же остается валидным и у вас есть дополнительный выбор передачи словаря options.

Найдите вычисляемое свойство coordinator и измените инициализацию постоянного хранилища вот так:

store = coordinator.addPersistentStoreWithType(
  NSSQLiteStoreType,
  configuration: nil,
  URL: storeURL,
  options: self.options)

Откройте NotesListViewController.swift и измените ленивую инициализацию CoreDataStack с использованием простой миграции:

lazy var stack : CoreDataStack = CoreDataStack(
  modelName:"UnCloudNotesDataModel",
  storeName:"UnCloudNotes",
  options:[NSMigratePersistentStoresAutomaticallyOption: true,
           NSInferMappingModelAutomaticallyOption: true])

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

Вторая часть, позволяющая проводить простые миграции - NSInferMappingModelAutomaticallyOption. Любая миграция нуждается в модели соответствия (mapping model). Вот вам, например, аналогия: когда вы путешествуете в незнакомую вам страну, то вы бы хотели иметь карту, по которой смогли бы ориентироваться. Вот как раз такой картой во время миграции и является модель соответствия.

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

  1. удаление сущностей, атрибутов или связей;
  2. изменение имен сущностей, атрибутов или связей, используя renamingIdentifier;
  3. добавление нового опционального атрибута;
  4. добавление нового, обязательного атрибута с дефолтным значением;
  5. изменение опционального атрибута на неопциональный, с указанием дефолтного значения;
  6. изменение неопционального атрибута на опциональный;
  7. изменение иерархии сущностей;
  8. добавление новой сущности-родителя, и сдвиг атрибутов вверх или вниз по иерархии;
  9. изменение связей с to-one на to-many;
  10. изменение связей с non-ordered to-many на ordered to-many (и наоборот);

Заметка:

Обратитесь к документации корпорации Apple для более полного представления о том, как Core Data выводит модель соответствия при простой миграции.

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

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

Запустите ваше приложение - ваши старые записи вернулись! Core Data перенесла ваше хранилище данных автоматически, используя модель соответствия. Значение для store, которое является не nil: то есть наличие сущности в логах, является отличным подтверждением того, что миграция была проведена и объект NSPersistentStore отображает хранилище.

Поздравляем! Вы только что провели миграцию своих первых данных!

Приложенные изображения

Ваши данные мигрированы, вам нужно обновить UI, чтобы была возможность добавления фотографий к записям. К счастью, большая часть работы была сделана за вас, таким образом вы можете сразу перейти к интересной части. ;]

Откройте Main.storyboard и пройдите в Create Note. Чуть ниже вы увидите сцену Create Note With Image, которая включает в себя интерфейс необходимый для наложения изображения.

Сцена Create Note присоединена к контроллеру навигации. Зажмите клавишу CTRL и перетащите от контроллера навигации (navigation controller) к сцене Create Note With Image, отпустите, и в появившемся меню выберите root view controller relationship сегвей. Это действие отсоединит старую сцену без изображения и подсоединит новую вместо нее:

Откройте AttachPhotoViewController.swift и добавьте туда следующий метод класса:

func imagePickerController(picker: UIImagePickerController,
 didFinishPickingMediaWithInfo info: [String: AnyObject]) {
  if let note = note {
    note.image = info[UIImagePickerControllerOriginalImage] as? UIImage
  }
  self.navigationController?.popViewControllerAnimated(true)
}

Этот метод распространит новое свойство на заметку, как только пользователь выберет что-либо из стандартной библиотеки изображений. Откройте CreateNoteViewController.swift и замените метод ViewDidAppear следующим кодом:

override func viewDidAppear(animated: Bool) {
  super.viewDidAppear(animated)
  if let image = note?.image {
    attachedPhoto.image = image
    view.endEditing(true)
  } else {
    titleField.becomeFirstResponder()
  }
}

Эта реализация отобразит новое изображение, если пользователь добавит его. Следующим шагом откройте NotesListViewController.swift и обновите метод tableView(_:cellForRowAtIndexPath) следующим:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -&gt; UITableViewCell {
  let note = notes.fetchedObjects?[indexPath.row] as? Note
  let identifier = note.image == nil ? "NoteCell" : "NoteCellImage"
 
  if let cell = tableView.dequeueReusableCellWithIdentifier(identifier, forIndexPath: indexPath) as NoteTableViewCell {
    cell.note = note
    return cell
  }
  return UITableViewCell()
}

Это установит корректный идентификатор ячейки, основываясь на отсутствии или наличии изображения в заметке. Добавьте следующие строки после кода в NoteTableViewCell.swift, который устанавливает дату создания заметки в updateNoteInfo():

if let image = note?.image {
  noteImage.image = image
}

Запустите ваше приложение и создайте заметку:

Нажмите Attach Image и добавьте изображение к заметке: выберите фотографию и вы увидите, что она будет добавлена:

Приложение использует стандартный UIImagePickerController для добавления фотографий в качестве приложений к заметке. Ура! Вы только что обновили вашу модель данных!

Заметка:

Для добавления ваших собственных изображений в симулятор, вам нужно перетащить в открытый альбом симулятора фотографию, которую вы хотите загрузить. Но если нет, то симулятор iOS 8 уже идет со встроенными фотографиями. :]       Если вы используете устройство, то откройте AttachPhotoViewController.swift и установите атрибут sourceType как Camera для того, чтобы у вас была возможность брать фотографии. Существующий код использует альбом, так как симулятор не имеет камеры.       Если у вас что-то пошло не так, то вы можете скачать финальный вариант проекта (swift 2.0)!

Что дальше?

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

Урок подготовил: Иван Акулов

Источник урока: http://www.raywenderlich.com/86136/lightweight-migrations-core-data-tuto...