【Core Data】LightWeight Migrationのやり方

## はじめに
個人でリリースしたアプリで`Core　Data`と`CloudKit`を使用しており、機能追加に伴い`Core　Data`を修正していくことになりました。**Core Data** のマイグレーションはドキュメントや記事を読む限り、そんなに難しくなさそうですが、しっかり理解したく記事にまとめました。

## 環境
Xcode 13.2.1
Swift 5.5.2

## ドキュメントを読む
ドキュメント：[Core Data - Light Weight Migration](https://developer.apple.com/documentation/coredata/using_lightweight_migration)
### Data Migrationの種類
**1. Light Weight (automatic) Migration**
Light Weight（自動）マイグレーションにより、アプリの変更に合わせたデータモデルの更新が可能。
**2. Heavy Weight (manual) Migration**
データモデルへの変更が自動マイグレーションの能力を超える場合は、Heavy Weight（手動）マイグレーションを使用する。

### Light Weight Migrationとは
ドキュメント：　[Using Lightweight Migration](https://developer.apple.com/documentation/coredata/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 に設定する。

```swift
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 から簡単に追加できます
![スクリーンショット 2022-03-01 20.36.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/25f592d2-5ec6-f39a-85ec-fca0eda66aba.png)
Version nameも初期値をそのまま使用し、Based on modelは現在のDataModelが指定されています
![スクリーンショット 2022-03-03 6.30.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/21c0ffb5-3a5d-4f97-6788-61db7941f1a2.png)
これで新しいDataModelが追加されました
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/67374eff-5140-a193-4b82-68ba50f70c40.png">

### DataModelの切り替え
`Model Version Current`を新しく追加したDataModelにします
![スクリーンショット 2022-03-03 6.42.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/81334f91-54b5-6a1d-d875-a5f4cf58bfcd.png)
緑色のマークが新しく追加したDataModelについていればOKです
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/5f5ad7a4-dcc8-a443-e770-fb80db8b1c65.png">

### 新しく追加したDataModelを好きに修正する

![スクリーンショット 2022-03-05 10.20.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/81056d98-0c51-711e-d151-b2d6e8af64c3.png)
👇今回はこのような形でEntityも新しく追加しました
![スクリーンショット 2022-03-05 10.20.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/306666d7-0c91-ded4-8e77-9e5cd269e672.png)

### 追加•修正したEntityのクラスを生成する
`dataModel`を選択して`Editor` -> `Create NSManagedObject Subclass`でEntityのクラスを自動生成する

その際追加したEntityの`Manual/None`にするのをお忘れなく💡
![スクリーンショット 2022-03-05 17.14.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/9e570b6b-af1d-13da-70e6-513b5523b123.png)


## 実装（結果必要なかった）
#### まずは現状の`CoreDataManager`の中を確認

```swift: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ができるかの確認（※ やらなくてもよさそう）
まずは、マッピングモデルが生成できるか確認してみます。

```swift:CoreDataManager
    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)`を書きデバッグで確認してみました。
![スクリーンショット 2022-03-05 13.29.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/3246eff6-0922-dffa-25ca-fca4cbdb4381.png)
それっぽいものが表示されているので、`Light Weight Migration`できそうなことが確認できました。

#### Light Weight Migrationの実装（※ 必要なかった）
まずは、現状(デフォルト）の`NSMigratePersistentStoresAutomaticallyOption`と`NSInferMappingModelAutomaticallyOption`を確認してみようと思いました。

```swift:CoreDataManager
let container = NSPersistentCloudKitContainer(name: "PadelLessonLog")
print(container.persistentStoreDescriptions.count)
print(container.persistentStoreDescriptions.first?.shouldMigrateStoreAutomatically)
print(container.persistentStoreDescriptions.first?.shouldInferMappingModelAutomatically)
```
ちょっと無理矢理ですが`print`を仕込んでデバッグしてみます。
![スクリーンショット 2022-03-05 14.53.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/90999050-e4b7-d995-1985-923eff310397.png)
どちらもすでに`true`で設定されていた❗️
どうやら修正の必要なくマイグレーションができそうです。



## おわりに
長々と書きましたが、結果的にCoreDataのマイグレーションは、新しいデータモデルを作成・切り替えするだけで済みそうです👏
しかし、マイグレーションに失敗して大切なデータが消えてしまったら大問題なので、このあたりは整理して慎重に作業していきたいと思いました。

## おまけ
こちらの記事に参考になりそうな部分がありました💡

https://williamboles.me/progressive-core-data-migration/


>Core Dataはまず、アプリのバンドル内で、永続ストアモデルから現在のバンドルモデルへマッピングするモデルを検索し、マイグレーションを実行しようとします。カスタムマッピングモデルが見つからない場合、Core Data は Lightweight マイグレーションを試みます。どちらの形式でも移行できない場合は、例外がスローされます。

## 参考

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreDataVersioning/Articles/vmUnderstandingVersions.html#//apple_ref/doc/uid/TP40004399-CH2-SW1

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreDataVersioning/Articles/vmModelFormat.html#//apple_ref/doc/uid/TP40004399-CH3-SW1

https://developer.apple.com/documentation/coredata/using_lightweight_migration

https://williamboles.me/progressive-core-data-migration/

https://qiita.com/ComeYummy/items/dafe0087d3ebb6068c57

https://chu-bura.hateblo.jp/entry/20190517/swift/coredata