[SwiftUI][CoreData] SwiftUI と MVVM で始める CoreData 入門 (その11:CoreData マイグレーションとテスト)

     
⌛️ 4 min.
SwiftUI と CoreData を組み合わせたアプリの作り方を説明します。

環境&対象

以下の環境で動作確認を行なっています。

  • 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 ファイルは、以下の手順で作成します。

  1. [File]-[New]-[File…]を選択
  2. “Choose a template for your new file:” で、”Core Data” から “Mapping Model” を選択し、”Next”
    Mapping Model 選択
    Mapping Model 選択
  3. “Mapping Model Source Data Model” で、MyTODO.xcdatamodel を選択
    Mapping Source を選択
    Mapping Source を選択
  4. “Mapping Model Target Data Model” で、ver2.xcdatamodel を選択
    Target Model 選択
    Target Model 選択
  5. 保存場所を選択して、作成終了

MappingModel 設定

先ほど作成した Mapping File を Xcode の Project Navigator(⌘1) で選択して、マッピングを設定していきます。

新しく追加した priority に、デフォルト値として 2 を設定します。(プログラム的に .middle を意味します)

priority に デフォルト値を設定
priority に デフォルト値を設定

この設定により、Ver1 のモデルを読み込んだ時には、priority に 2 が設定されるようになります。

MappingModel のテスト

動くようになったハズですが、テストを行って確認します。

テストの方向性

CoreData のマイグレーションは、CoreData のデータ/ファイル に対して動作するものなので、CoreData のデータに対して行います。

マイグレーションのテストを行うためには、Source にあたる Ver1 のデータが必要となります。

データを用意する方法として、Ver1で作成したファイルを保存しておいて使う方法と コード上でデータを作る方法があります。
今回は、コード上で、データを作っていきます。

作ったデータを、Ver2 で読み込んで 期待するデータになっているかを確認します。

Ver1 モデルとデータ作成

CoreData のモデルファイルは、<プロジェクト名>.xcdatamodeld となっていますが、実は Bundle になっていて、過去のバージョンのモデルも保持することができます。

Xcode の Project Navigator でも確認できます。

2つのモデルの存在
2つのモデルの存在

このモデルを使って、Ver1 のモデルを作成していきます。

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)
コード解説
  1. Bundle からモデルファイルを読込、NSPersistentStoreCoordinator, NSManagedObjectContext と作っていきます(Ver1 です)
  2. テンポラリディレクトリに作成していますが、念の為、全要素を削除します
  3. テストしたい Entity を作成し、必要な属性値をセットします
  4. Entity が作成できたことを確認します

Ver1 のデータを作成できたので、Ver2 へ変換し その結果をテストします。

Ver2 モデルとデータ読込

Ver2 も同様に、Bundle にモデルの情報が保持されています。

Ver1データを Ver2 へマイグレーションしテスト


        // (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
コード解説
  1. Ver2 のモデルを準備しています。(Ver1 とほとんど同じです)
  2. Ver1 のデータファイルを Ver2 へ読み込ませることで、マイグレーションを発生させています
  3. マイグレーション後のデータを取得し、属性値が期待する値となっているかをテストします

今回のテストは、新しい属性値がデフォルト値に設定されているかをテストしていますが、関係(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 の追加

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です