はじめに
個人でリリースしたアプリでCore Data
とCloudKit
を使用しており、機能追加に伴いCore Data
を修正していくことになりました。Core Data のマイグレーションはドキュメントや記事を読む限り、そんなに難しくなさそうですが、しっかり理解したく記事にまとめました。
環境
Xcode 13.2.1
Swift 5.5.2
ドキュメントを読む
ドキュメント:Core Data - Light Weight Migration
Data Migrationの種類
1. Light Weight (automatic) Migration
Light Weight(自動)マイグレーションにより、アプリの変更に合わせたデータモデルの更新が可能。
2. Heavy Weight (manual) Migration
データモデルへの変更が自動マイグレーションの能力を超える場合は、Heavy Weight(手動)マイグレーションを使用する。
Light Weight Migrationとは
ドキュメント: Using Lightweight Migration
Core Dataは通常、軽量マイグレーションと呼ばれる自動データ移行を行うことができます。軽量マイグレーションは、移行元と移行先のマネージドオブジェクトモデルの違いからデータ移行を推測して行う。
■ Core Dataは以下の条件で、永続的なエンティティやプロパティに対するスキーマの変更を分析し、推論されたマッピングモデルを生成しデータ移行する。
- 属性(Attribute)の追加
- 属性(Attribute)の削除
- 属性(Attribute)をオプショナルへ変更
- 属性(Attribute)を非オプショナルへ変更(デフォルト値設定)
- エンティティ・プロパティの名前変更
■ エンティティやプロパティの名前を変更した場合、変更先モデルの名前変更識別子を、変更元モデルの対応するプロパティやエンティティの名前に設定することでデータ移行を行う。
■ 新しいリレーションシップを追加したり、既存のリレーションシップを削除したりすることができる。また、属性と同じように、リネーム識別子を使用してリレーションシップの名前を変更することもできる。さらに、リレーションを一対一から多対一に変更したり、非順序型から順序型に変更したり(その逆も可能)することができる。
■ 階層内のエンティティの追加、削除、名前の変更が可能。また、新しい親エンティティまたは子エンティティを作成し、エンティティ階層の上下にプロパティを移動することができる。階層からエンティティを移動することができる。ただし、エンティティ階層をマージすることはできない。既存の2つのエンティティがソースで共通の親を共有していない場合、それらのエンティティがデスティネーションで共通の親を共有することはできない。
Light Weight Migrationができるか確認する
実際に移行作業を行わずに、Core Dataが移行元モデルと移行先モデルの間のマッピングモデルを推論できるかどうかを事前に判断したい場合は、NSMappingModel
のinferredMappingModel(forSourceModel:destinationModel:)
メソッドを使用し、Core Data が作成可能な場合は推論されたモデルが取得できる。
Light Weight Migrationの実装方法
addPersistentStore(ofType: configurationName: at: options:)
を使用して、自動軽量化移行を要求する。尚、パラメータで渡すオプションはNSMigratePersistentStoresAutomaticallyOption
および NSInferMappingModelAutomaticallyOption
の両方のキーに対応する値を true に設定する。
let psc = NSPersistentStoreCoordinator(managedObjectModel: mom)
let options = [NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true]
do {
try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options)
} catch {
fatalError("Failed to add persistent store: \(error)")
}
これらの設定により、Core Dataは永続ストアが現在のモデルにマッチしなくなったことを検出すると、軽量マイグレーションを試行します。
以上がドキュメントから確認できた内容です。
新しいDataModelの追加と切り替え
DataModelの追加
Editor > Add Model Version から簡単に追加できます
Version nameも初期値をそのまま使用し、Based on modelは現在のDataModelが指定されています
これで新しいDataModelが追加されました
DataModelの切り替え
Model Version Current
を新しく追加したDataModelにします
緑色のマークが新しく追加したDataModelについていればOKです
新しく追加したDataModelを好きに修正する
追加•修正したEntityのクラスを生成する
dataModel
を選択してEditor
-> Create NSManagedObject Subclass
でEntityのクラスを自動生成する
その際追加したEntityのManual/None
にするのをお忘れなく💡
実装(結果必要なかった)
まずは現状のCoreDataManager
の中を確認
final class CoreDataManager: CoreDataProtocol {
static let shared = CoreDataManager()
private init() { }
// アプリケーション内のオブジェクトとデータベースの間のやり取りを行う
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "PadelLessonLog")
container.loadPersistentStores(completionHandler: { _, error in
if let error = error as NSError? {
// リリースビルドでは通っても何も起きない
assertionFailure("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// persistentContainerデータベース情報を表す
// 管理オブジェクトコンテキスト。NSManagedObject 群を管理するクラス
var managerObjectContext: NSManagedObjectContext {
persistentContainer.viewContext
}
}
Core Dataをプロジェクト新規作成時のオプションで追加するとAppDelegate
に一部コードが追加されますが、その部分をCoreDataManager
に移してあります。
Light Weight Migrationができるかの確認(※ やらなくてもよさそう)
まずは、マッピングモデルが生成できるか確認してみます。
var checkLightWeightMigration: NSMappingModel {
let subdirectory = "PadelLessonLog.momd"
let sourceModel = NSManagedObjectModel(contentsOf: Bundle.main.url(forResource: "PadelLessonLog", withExtension: "mom", subdirectory: subdirectory)!)!
let destinationModel = NSManagedObjectModel(contentsOf: Bundle.main.url(forResource: "PadelLessonLog 2", withExtension: "mom", subdirectory: subdirectory)!)!
do {
return try NSMappingModel.inferredMappingModel(forSourceModel: sourceModel, destinationModel: destinationModel)
} catch {
fatalError("migrationCheck error \(error)")
}
}
CoreDataManager
に👆を足して、適当な場所にprint(CoreDataManager.shared.checkLightWeightMigration)
を書きデバッグで確認してみました。
それっぽいものが表示されているので、Light Weight Migration
できそうなことが確認できました。
Light Weight Migrationの実装(※ 必要なかった)
まずは、現状(デフォルト)のNSMigratePersistentStoresAutomaticallyOption
とNSInferMappingModelAutomaticallyOption
を確認してみようと思いました。
let container = NSPersistentCloudKitContainer(name: "PadelLessonLog")
print(container.persistentStoreDescriptions.count)
print(container.persistentStoreDescriptions.first?.shouldMigrateStoreAutomatically)
print(container.persistentStoreDescriptions.first?.shouldInferMappingModelAutomatically)
ちょっと無理矢理ですがprint
を仕込んでデバッグしてみます。
どちらもすでにtrue
で設定されていた❗️
どうやら修正の必要なくマイグレーションができそうです。
おわりに
長々と書きましたが、結果的にCoreDataのマイグレーションは、新しいデータモデルを作成・切り替えするだけで済みそうです👏
しかし、マイグレーションに失敗して大切なデータが消えてしまったら大問題なので、このあたりは整理して慎重に作業していきたいと思いました。
おまけ
こちらの記事に参考になりそうな部分がありました💡
Core Dataはまず、アプリのバンドル内で、永続ストアモデルから現在のバンドルモデルへマッピングするモデルを検索し、マイグレーションを実行しようとします。カスタムマッピングモデルが見つからない場合、Core Data は Lightweight マイグレーションを試みます。どちらの形式でも移行できない場合は、例外がスローされます。
参考