Sponsor Link
環境&対象
- macOS Big Sur 11.1
- Xcode 12.3
- iOS 14.2
CoreData マイグレーション
前回、新しいプロパティ Priority を導入して、TODOItem にプライオリティを持つようにしました。
Ver1 の時には、Priorityがなかったので、現在のアプリで Ver1 のデータを読み込むことはできません。
つまり、Ver1 のデータは、そのままでは Ver2 で使うことはできません。
開発段階では許されるかもしれませんが、一度リリースしたアプリでは、新バージョンで 過去バージョンのデータが読み込めることは重要です。
そのための方法として、「マイグレーション」する方法が用意されています。
今回の新しいプロパティ Priority は他のプロパティ値と深い関連性はないので、Ver2 のモデルに読み込む際には、デフォルト値を割り当てて使うことにします。(.middle を設定します)
マイグレーションとは?
Ver1 モデルと Ver2 モデルの関係性(マッピング)を定義することで、Ver1 のデータを読み込むことができるようにします。
マイグレーションには、Lightweight Migration と Heavyweight Migration の2種類があります。
Heavyweight Migration は、変換を定義して行うもので、Apple のドキュメントでは、Lightweight Migration で扱えない時に使うべき方法と説明されています。
今回は、新しく追加されたプロパティ Priority に対して、Ver2 で デフォルト値を設定します。Lightweight Migration でも対応できるものですが、Heavyweight Migration で行っていきます。
Apple のドキュメント Lightweight Migration はこちら。
Apple のドキュメント Heavyweight Migration はこちら。
MappingModel の作成
Heavyweight Migration では、Xcode で作成する mapping ファイルを使用します。
MappingModel ファイルの作成
Mapping ファイルは、以下の手順で作成します。
MappingModel 設定
先ほど作成した Mapping File を Xcode の Project Navigator(⌘1) で選択して、マッピングを設定していきます。
新しく追加した priority に、デフォルト値として 2 を設定します。(プログラム的に .middle を意味します)

この設定により、Ver1 のモデルを読み込んだ時には、priority に 2 が設定されるようになります。
MappingModel のテスト
動くようになったハズですが、テストを行って確認します。
テストの方向性
CoreData のマイグレーションは、CoreData のデータ/ファイル に対して動作するものなので、CoreData のデータに対して行います。
マイグレーションのテストを行うためには、Source にあたる Ver1 のデータが必要となります。
データを用意する方法として、Ver1で作成したファイルを保存しておいて使う方法と コード上でデータを作る方法があります。
今回は、コード上で、データを作っていきます。
作ったデータを、Ver2 で読み込んで 期待するデータになっているかを確認します。
Ver1 モデルとデータ作成
CoreData のモデルファイルは、<プロジェクト名>.xcdatamodeld となっていますが、実は Bundle になっていて、過去のバージョンのモデルも保持することができます。
Xcode の Project Navigator でも確認できます。

このモデルを使って、Ver1 のモデルを作成していきます。
// prep ver1 model
// (1)
let v1ModelURL = Bundle.main.url(forResource: "MyTODO", withExtension: "mom", subdirectory: "MyTODO.momd")!
let v1Model = NSManagedObjectModel(contentsOf: v1ModelURL)!
let v1Coordinator = NSPersistentStoreCoordinator(managedObjectModel: v1Model)
let v1DataFileUrl = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("v1.sqlite")
let v1StoreDesc = NSPersistentStoreDescription(url: v1DataFileUrl)
v1StoreDesc.shouldAddStoreAsynchronously = false
v1Coordinator.addPersistentStore(with: v1StoreDesc) { (_, error) in
if error != nil { XCTFail("failed to addPersistentStore") }
}
let v1moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
v1moc.persistentStoreCoordinator = v1Coordinator
// (2)
// delete all
let deleteRequest = NSBatchDeleteRequest(fetchRequest: NSFetchRequest(entityName: "CDTODOItem") as! NSFetchRequest)
try v1moc.execute(deleteRequest)
// (3)
// add v1 element
let entity1 = NSEntityDescription.insertNewObject(forEntityName: "CDTODOItem", into: v1moc) as! CDTODOItem
entity1.id = UUID()
entity1.title = "Title"
entity1.detail = "Detail"
entity1.isDone = false
try v1moc.save()
// (4)
// check
let resultOnV1 = try v1moc.fetch(NSFetchRequest(entityName: "CDTODOItem"))
XCTAssertEqual(resultOnV1.count, 1)
- Bundle からモデルファイルを読込、NSPersistentStoreCoordinator, NSManagedObjectContext と作っていきます(Ver1 です)
- テンポラリディレクトリに作成していますが、念の為、全要素を削除します
- テストしたい Entity を作成し、必要な属性値をセットします
- Entity が作成できたことを確認します
Ver1 のデータを作成できたので、Ver2 へ変換し その結果をテストします。
Ver2 モデルとデータ読込
Ver2 も同様に、Bundle にモデルの情報が保持されています。
// (1)
// setup v2
let v2ModelURL = Bundle.main.url(forResource: "ver2", withExtension: "mom", subdirectory: "MyTODO.momd")!
let v2Model = NSManagedObjectModel(contentsOf: v2ModelURL)!
let v2Coordinator = NSPersistentStoreCoordinator(managedObjectModel: v2Model)
let v2StoreDesc = NSPersistentStoreDescription(url: v1DataFileUrl)
v2StoreDesc.shouldAddStoreAsynchronously = false
// (2)
// migrate
v2Coordinator.addPersistentStore(with: v1StoreDesc) { (_, error) in
if error != nil { XCTFail("Failed to addPersistentStore v2")}
}
let v2moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
v2moc.persistentStoreCoordinator = v2Coordinator
// (3)
// fetch new element
let result = try v2moc.fetch(NSFetchRequest(entityName: "CDTODOItem"))
XCTAssertEqual(result.count, 1)
let entityv2 = result[0] //try XCTUnwrap(result[0] as? CDTODOItem)
XCTAssertEqual(entityv2.id, entity1.id)
XCTAssertEqual(entityv2.title, entity1.title)
XCTAssertEqual(entityv2.detail, entity1.detail)
XCTAssertEqual(entityv2.isDone, entity1.isDone)
XCTAssertEqual(entityv2.priority, 2) // migration target
- Ver2 のモデルを準備しています。(Ver1 とほとんど同じです)
- Ver1 のデータファイルを Ver2 へ読み込ませることで、マイグレーションを発生させています
- マイグレーション後のデータを取得し、属性値が期待する値となっているかをテストします
今回のテストは、新しい属性値がデフォルト値に設定されているかをテストしていますが、関係(Relation) 等が使用されるマイグレーションであれば、そのようなデータを作成してマイグレーションテストを行う必要があります。
念のため、テストコード全体を以下に引用します。
//
// ModelMigration_Tests.swift
//
// Created by : Tomoaki Yagishita on 2020/12/23
// © 2020 SmallDeskSoftware
//
import XCTest
import CoreData
@testable import MyTODO
class ModelMigration_Tests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func test_migration_fromV1toV2_PriorityShouldBeMiddle() throws {
// prep ver1 model
let v1ModelURL = Bundle.main.url(forResource: "MyTODO", withExtension: "mom", subdirectory: "MyTODO.momd")!
let v1Model = NSManagedObjectModel(contentsOf: v1ModelURL)!
let v1Coordinator = NSPersistentStoreCoordinator(managedObjectModel: v1Model)
let v1DataFileUrl = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("v1.sqlite")
let v1StoreDesc = NSPersistentStoreDescription(url: v1DataFileUrl)
v1StoreDesc.shouldAddStoreAsynchronously = false
v1Coordinator.addPersistentStore(with: v1StoreDesc) { (_, error) in
if error != nil { XCTFail("failed to addPersistentStore") }
}
let v1moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
v1moc.persistentStoreCoordinator = v1Coordinator
// delete all
let deleteRequest = NSBatchDeleteRequest(fetchRequest: NSFetchRequest(entityName: "CDTODOItem") as! NSFetchRequest)
try v1moc.execute(deleteRequest)
// add v1 element
let entity1 = NSEntityDescription.insertNewObject(forEntityName: "CDTODOItem", into: v1moc) as! CDTODOItem
entity1.id = UUID()
entity1.title = "Title"
entity1.detail = "Detail"
entity1.isDone = false
try v1moc.save()
// check
let resultOnV1 = try v1moc.fetch(NSFetchRequest(entityName: "CDTODOItem"))
XCTAssertEqual(resultOnV1.count, 1)
// setup v2
let v2ModelURL = Bundle.main.url(forResource: "ver2", withExtension: "mom", subdirectory: "MyTODO.momd")!
let v2Model = NSManagedObjectModel(contentsOf: v2ModelURL)!
let v2Coordinator = NSPersistentStoreCoordinator(managedObjectModel: v2Model)
let v2StoreDesc = NSPersistentStoreDescription(url: v1DataFileUrl)
v2StoreDesc.shouldAddStoreAsynchronously = false
// migrate
v2Coordinator.addPersistentStore(with: v1StoreDesc) { (_, error) in
if error != nil { XCTFail("Failed to addPersistentStore v2")}
}
let v2moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
v2moc.persistentStoreCoordinator = v2Coordinator
// fetch new element
let result = try v2moc.fetch(NSFetchRequest(entityName: "CDTODOItem"))
XCTAssertEqual(result.count, 1)
let entityv2 = result[0] //try XCTUnwrap(result[0] as? CDTODOItem)
XCTAssertEqual(entityv2.id, entity1.id)
XCTAssertEqual(entityv2.title, entity1.title)
XCTAssertEqual(entityv2.detail, entity1.detail)
XCTAssertEqual(entityv2.isDone, entity1.isDone)
XCTAssertEqual(entityv2.priority, 2) // migration target
}
}
マイグレーションのテストを行ったので、次回は、機能追加していきます。
- Priority 順ソートの追加
- UNDO/REDO の追加
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link