Когда в приложении меняется модель данных, миграция становится головной болью. Особенно если пользователи уже накопили тонны информации, которую нельзя просто выкинуть.
SwiftData предлагает несколько способов справиться с этой задачей: от автоматических легких миграций до полностью ручных, где разработчик контролирует каждый шаг. Разбираемся, как не наступить на грабли.
Версионирование с первого дня:
Любая работа с данными должна начинаться с версионирования. Даже если у вас сейчас одна-единственная модель, ее стоит обернуть в
VersionedSchema. Это создает стабильную точку отсчета. Потом, когда появятся изменения, будет понятно, откуда и куда мигрировать.
Обычно схемам дают номера версий:
V1,
V2,
V3. В коде они живут как отдельные
enum со списком моделей и идентификатором версии. А для удобства в основном коде используют
typealias, чтобы каждый раз не писать
ExerciseSchemaV5.Exercise.
Новую версию стоит заводить перед каждым релизом, в котором меняются модели. Даже если изменений несколько, они все могут войти в одну версию схемы. Главное чтобы пользователи, пропустившие пару обновлений, могли переехать сразу в актуальную версию без потери данных.
Когда SwiftData справляется сама:
Есть изменения, которые
SwiftData умеет обрабатывать автоматически. К ним относятся:
- Добавление опционального поля (старым объектам просто ставится nil).
- Удаление поля (данные теряются, но миграция проходит гладко).
- Переименование поля (если указать @Attribute(originalName:)).
В этих случаях можно либо вообще не писать миграционный план, либо добавить его для порядка, но использовать легкий этап
.lightweight. План может пригодиться, когда хочется четко контролировать все шаги и тестировать их.
Когда нужна ручная работа:
Легкая миграция перестает работать, если новое поле обязательное и без значения по умолчанию.
SwiftData не может придумать, что туда записать. То же самое когда нужно преобразовать данные: изменить тип, разбить одно поле на несколько, смержить сущности, почистить дубликаты.
В таких случаях пишется
SchemaMigrationPlan с кастомными этапами. У каждого этапа есть две фазы:
- willMigrate - выполняется до применения новой схемы, работает со старыми данными (можно почистить дубликаты).
- didMigrate - после применения, здесь уже доступны новые модели и можно заполнять поля.
Например, если в новой версии появилось обязательное поле
createdAt, в
didMigrate проходим по всем объектам и проставляем дату.
Сложные случаи - стратегия мостовой версии:
Иногда изменения настолько серьезные, что впихнуть все в одну миграцию рискованно. Например, в старой модели хранились вес, повторения и подходы в одном объекте, а в новой нужно разнести это по связанным сущностям.
Здесь выручает промежуточная версия. На первом шаге добавляем новые поля и отношения, но старые пока оставляем (можно переименовать, чтобы не конфликтовали). В миграции заполняем новые объекты из старых данных. А в следующей версии уже спокойно удаляем устаревшие поля - это будет легкая миграция.
Такой подход кажется более громоздким, но зато безопасным. Данные не теряются, каждый шаг тестируется отдельно.
Вывод:
Миграции в
SwiftData - это не про героизм, а про аккуратность. Если с самого начала заложить версионирование и продумывать изменения, большинство проблем решаются либо автоматически, либо небольшими кастомными этапами. А для самых сложных случаев всегда можно разбить задачу на несколько версий, чтобы не переписывать все за один раз. Главное - не забывать тестировать на реальных данных, а не только на пустой базе в симуляторе.