2012/05/12更新

[XCODE] CoreDataにおいてテーブル定義変更を行う方法

このエントリーをはてなブックマークに追加      

今日は、iPhone開発ネタのうち、CoreDataのお話です。
CoreDataを用いたデータの永続化は、多くのアプリで行われていると思います。
しかし、一度テーブル定義を行ってから、その後テーブル定義変更を行い、その後アプリを 実行するとエラーが。そのエラーを発生させず、定義変更後にデータを移行する方法を 今日は書きたいと思います。

Core Data Migration Image




CoreDataのデータ移行(マイグレーション)の必要性

私がリリースしているiPhoneアプリの中に、出費管理を行う Pocket.Money.Managementがあります。 このアプリでは、出費情報(日付、内容、金額)をCoreDataを用いて保存して、参照するアプリです。
既にリリースしているのですが、このたびテーブルを一つ追加したいと思いました。

その際に、既に定義済みの定義ファイルを変更すると以下のようなエラーが発生します。
    2012-05-08 08:31:10.883 PMP_tmp[1180:fe03] Unresolved error Error Domain=NSCocoaErrorDomain Code=134130 "The operation couldn’t be completed. (Cocoa error 134130.)" UserInfo=0x6b8e5f0 {URL=file://localhost/Users/someone/Library/Application%20Support/iPhone%20Simulator/5.1/Applications/E7154F8A-8145-40CE-8ED3-FEF82D39055D/Documents/PocketMoneyManagement.sqlite, metadata=<CFBasicHash 0x6b71920 [0x1ed8b48]>{type = immutable dict, count = 7,
    entries =>
    2 : <CFString 0x6b71b00 [0x1ed8b48]>{contents = "NSStoreModelVersionIdentifiers"} = <CFArray 0x6b721f0 [0x1ed8b48]>{type = immutable, count = 1, values = (
    0 : <CFString 0x1ed3cd8 [0x1ed8b48]>{contents = ""}
    )}
    4 : <CFString 0x6b71d30 [0x1ed8b48]>{contents = "NSPersistenceFrameworkVersion"} = <CFNumber 0x6b71d90 [0x1ed8b48]>{value = +386, type = kCFNumberSInt64Type}
    6 : <CFString 0x6b71d60 [0x1ed8b48]>{contents = "NSStoreModelVersionHashes"} = <CFBasicHash 0x6b723a0 [0x1ed8b48]>{type = immutable dict, count = 4,
    entries =>
    0 : <CFString 0x6b72240 [0x1ed8b48]>{contents = "Content"} = <CFData 0x6b72350 [0x1ed8b48]>{length = 32, capacity = 32, bytes = 0x8e9cd0ae096564b4a809b320b403172a ... 414356e3574f9d65}
    3 : <CFString 0x6b71b50 [0x1ed8b48]>{contents = "Result"} = <CFData 0x6b722b0 [0x1ed8b48]>{length = 32, capacity = 32, bytes = 0x269fc7c2219f67a0999c5b6ba5e4358a ... d1d5608e4862793a}
    5 : <CFString 0x6b72230 [0x1ed8b48]>{contents = "Amount"} = <CFData 0x6b72300 [0x1ed8b48]>{length = 32, capacity = 32, bytes = 0x24a58d8fb0e99f8b175fb3a9c58343f4 ... c0861d5accd4f241}
    6 : <CFString 0x6b72210 [0x1ed8b48]>{contents = "Setting"} = <CFData 0x6b72260 [0x1ed8b48]>{length = 32, capacity = 32, bytes = 0xe464427daef6937e3a9cdf48cafba8e5 ... 91729a25ffbe4090}
    }
    
    7 : <CFString 0x1165ad8 [0x1ed8b48]>{contents = "NSStoreUUID"} = <CFString 0x6b71f50 [0x1ed8b48]>{contents = "C0CADBB5-E31C-46AD-893B-0B14D99259E5"}
    8 : <CFString 0x1165978 [0x1ed8b48]>{contents = "NSStoreType"} = <CFString 0x1165988 [0x1ed8b48]>{contents = "SQLite"}
    9 : <CFString 0x6b721a0 [0x1ed8b48]>{contents = "_NSAutoVacuumLevel"} = <CFString 0x6b72420 [0x1ed8b48]>{contents = "2"}
    10 : <CFString 0x6b721c0 [0x1ed8b48]>{contents = "NSStoreModelVersionHashesVersion"} = <CFNumber 0x6b30d10 [0x1ed8b48]>{value = +3, type = kCFNumberSInt32Type}
    }
    , reason=Can't find model for source store}, {
    URL = "file://localhost/Users/someone/Library/Application%20Support/iPhone%20Simulator/5.1/Applications/E7154F8A-8145-40CE-8ED3-FEF82D39055D/Documents/PocketMoneyManagement.sqlite";
    metadata =     {
    NSPersistenceFrameworkVersion = 386;
    NSStoreModelVersionHashes =         {
    Amount = <24a58d8f b0e99f8b 175fb3a9 c58343f4 9bf16d35 ad5e07ac c0861d5a ccd4f241>;
    Content = <8e9cd0ae 096564b4 a809b320 b403172a 24218049 36a6fa77 414356e3 574f9d65>;
    Result = <269fc7c2 219f67a0 999c5b6b a5e4358a 2da8d836 69806579 d1d5608e 4862793a>;
    Setting = <e464427d aef6937e 3a9cdf48 cafba8e5 ddf8a144 0949e9fe 91729a25 ffbe4090>;
    };
    NSStoreModelVersionHashesVersion = 3;
    NSStoreModelVersionIdentifiers =         (
    ""
    );
    NSStoreType = SQLite;
    NSStoreUUID = "C0CADBB5-E31C-46AD-893B-0B14D99259E5";
    "_NSAutoVacuumLevel" = 2;
    };
    reason = "Can't find model for source store";
}

たくさんのデバッグ情報が出ますが、「reason = "Can't find model for source store"」が気にする部分のようです。


このエラーが出た際に、iPhone実機やシミュレーター上に前のビルドから残っているアプリを消してから、 もう一度起動するという手も新規開発中ならOKですが、既にリリースしたアプリの場合には、 CoreDataのモデルのバージョン移行が必要になります。




CoreDataのバージョン移行を行う手順

CoreDataでテーブル定義を変更する方法は、そんなに難しくなく、以下の手順で実施可能です。
なお今回は、既にver1とver2のCoreDataが存在する状態で、ver1→ver2, ver1→ver3にデータを移行する例となっております。
初めてver2を作る場合も、一緒なので参考になると嬉しいです。

新しいモデルを追加する

まずは、メニューバーで「Editor → Add Model Version」を選択します。
画像

続いて、モデル名を記入して、拡張するベースとなるモデルを選択します。
ここで記入するモデル名は、バージョン番号などを入れると良いかもしれません。
画像

Finishボタンをクリックすると、以下のようにxcdatamodelIdの下に、作成したモデルバージョンが追加されます。
画像


追加したモデルバージョンで変更したいようにテーブル定義を変更する

続いて、上記で追加したモデルバージョンに対して、今回変えたい内容を記載します。
以下の例では、新しく「NewEntity」というテーブルを新規追加しています。
画像


移行方法を記載したファイルを作成する

今回追加したバージョンにデータを移行する方法を記載したファイルを作成します。
ここで重要なのは、ver1, ver2が既にある場合には、ver1→ver3、ver2→ver3というように、既存のバージョン全てから 今回追加したバージョンへ移行する為のファイルを記載する必要がある点です。
ユーザーによっては、アプリをUpdateしてなくて、ver1のデータモデルのアプリを使い続けてるかもしれないので。
移行定義を行うファイルは、「File > New > File...」から追加できます。
画像

ファイル追加ダイアログを開いたら、CoreDataにあるMapping Modelを選択し、Nextボタンを押します。
画像

次のページでは、移行元のバージョンを選択します。今回は、ver1を移行元バージョンとして選択しています。
画像

次に、移行先のバージョンを選択します。今回は、ver3を移行先として選択します。
画像

最後にファイル名を入力します。ファイル名は自由に設定出来ますが、ver1_to_ver2などの移行元と先が分かると便利かもしれません。
画像

作成すると以下のような内容のファイルが作成されます。自動的に移行元と移行先が定義されますが、変更したい場合には 自分で変更することもできます。
画像


データ移行を行う実装を行う

今まで作成したデータ移行用のファイルを、アプリ起動時に読み込んで実行してもらう為に、 ソースコードの記載を変更します。
CoreDataを読み込む部分(XCODEプロジェクトで作るとApplicationDelegateを実装したクラスの中とか)で、 読み込む処理を変更します。
(変更前のソースコード)
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    
    if (persistentStoreCoordinator_ != nil) {
        return persistentStoreCoordinator_;
    }
    
    NSURL *storeURL = [NSURL fileURLWithPath: [[self applicationDocumentsDirectory] stringByAppendingPathComponent: @"PocketMoneyManagement.sqlite"]];
    
    NSError *error = nil;
    persistentStoreCoordinator_ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    
    if (![persistentStoreCoordinator_ addPersistentStoreWithType:NSSQLiteStoreType 
                                                   configuration:nil 
                                                             URL:storeURL 
                                                         options:nil error:&error]) {

        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }    
    
    return persistentStoreCoordinator_;
}

上記のソースコードのうち、「addPersistentStoreWithType:configuration:URL:options:error:」メソッド呼び出しの部分で、 optionsを指定して、データ移行を行うように設定します。
変更後のソースコードは以下となります。
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    
    if (persistentStoreCoordinator_ != nil) {
        return persistentStoreCoordinator_;
    }
    
    NSURL *storeURL = [NSURL fileURLWithPath: [[self applicationDocumentsDirectory] stringByAppendingPathComponent: @"PocketMoneyManagement.sqlite"]];
    
    NSError *error = nil;
    persistentStoreCoordinator_ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];


    /*********** 移行を行う為のoptionsを生成する  ***********/    
    NSDictionary *options = [NSDictionary 
                             dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES],NSMigratePersistentStoresAutomaticallyOption, 
                             [NSNumber numberWithBool:YES], 
                             NSInferMappingModelAutomaticallyOption, 
                             nil];
    
    /*********** options指定ありでCoreDataを読み込む  **********/
    if (![persistentStoreCoordinator_ addPersistentStoreWithType:NSSQLiteStoreType 
                                                   configuration:nil 
                                                             URL:storeURL 
                                                         options:options error:&error]) {

        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }    
    
    return persistentStoreCoordinator_;
}

NSDictionaryでoptionsを作成して、自動的にデータ移行を行うように設定しています。




参考資料

以下の資料を参考にしました。もっと詳しい情報は、以下を参照下さい。
Core Data Versioning.pdf@Apple Developer



最後に

CoreDataのマイグレーションはもっと難しいかと思いましたが、手順さえ踏めば簡単なんだと 知ることが出来て良かったです。
自身のアプリを進化させるべく、今後もiPhoneアプリ開発を頑張利隊と思います。
そこで得た情報をまたブログに記載させて頂きたいと思います。
いつもの2倍以上の記事量でしたが、最後までお読み頂きまして本当にありがとうございます。






こんな記事もいかがですか?

RSS画像

もしご興味をお持ち頂けましたら、ぜひRSSへの登録をお願い致します。