Core Dataのマイグレーション(手動編)

in

既存のマッピングで対応できないような場合、NSEntityMigrationPolicyを拡張してカスタマイズできることはわかったのですがドキュメントを読むだけではいまひとつピンとこなかったので実際にやってみました。トライしたのは下記のアドレス帳モデルの変換。とりあえず、v1のモデルで固定・携帯の2つの電話を持つ桃太郎、携帯電話のみを持つ浦島太郎、電話は持たない一寸法師のアドレス帳データを作成しこれをv2モデルに変換します。マイグレーションの骨子は、

  1. 電話を持たない人はそのまま新しいAddressBookエンティティにマッピング
  2. 電話を持つ人はPhone1/Phone2をPhoneBookエンティティにマッピング

の2点。1に関しては通常のマッピングで対応し、2に関してマイグレーションポリシーを拡張します。

まず、v1のAddressBookエンティティにあるphone1とphone2をPhoneBookエンティティに移行するため新しいマッピングを適当な名前で作成します。ここではAddressBookToPhoneBookという名前のマッピングモデルを作成しました。ソースにはAddressBook、デスティネーションにはPhoneBookを指定します。また、電話を持たないAddressBookインスタンスはPhoneBookへのマッピングは不要なのでフィルタ述語に

phone1 != null || phone2 != null

を指定します。

AddressBookからAddressBookへのマイグレーションに関してはphone1/phone2がなくなったのでマッピングからもなくなります。代わりにphonesという関連をマッピングする必要がありますがデフォルトで作成されているAddressBookToAddressBookのphonesマッピングは空欄になっているのでここを埋めます。キーパスには自分自身を示す$source、マッピングにはphone1/phone2から作成されたPhoneBookエンティティを指定しますが、これはまさに先に作ったAddressBookToPhoneBookということになります。

マッピング作業はここまで。後は、NSEntityMigrationPolicyを拡張してマッピングの実体を実装します。

カスタムポリシーはドキュメントにあるように

  1. beginEntityMapping:manager:error:
  2. createDestinationInstancesForSourceInstance:entityMapping:manager:error:
  3. endInstanceCreationForEntityMapping:manager:error:
  4. createRelationshipsForDestinationInstance:entityMapping:manager:error:
  5. endRelationshipCreationForEntityMapping:manager:error:
  6. performCustomValidationForEntityMapping:manager:error:
  7. endEntityMapping:manager:error:

のメソッドをオーバーライドしていきます。実装のメインはcreateDestinationInstancesForSourceInstance:で、このメソッドはマイグレーション対象になる桃太郎・浦島太郎のインスタンス毎にそれぞれ一度ずつ呼び出されます。そこでPhoneBookを作成する処理を書くわけです。

電話番号情報は引数で取得するNSManagedObject(つまりAddressBookエンティティ)のインスタンスから通常の方法で取得します。

NSString *sPhone1 = [sInstance valueForKey:@"phone1"];

この電話を元にPhoneBookエンティティをインスタンス化します。引数でNSMigrationManagerが与えられていますからこれを使用します。

NSManagedObjectContext *moc = [manager destinationContext];
NSManagedObject *dPhone1 = [NSEntityDescription 
  insertNewObjectForEntityForName:@"PhoneBook" inManagedObjectContext:moc];
[dPhone1 setValue:sPhone1 forKey:@"phoneNumber"];

最後に作成したPhoneBookのインスタンスをAddressBookのPhonesに関連づけします。

[manager associateSourceInstance:sInstance 
  withDestinationInstance:dPhone1 forEntityMapping:mapping];

AddressBookToAddressBookのphnesマッピングの指定とこのしたこのassociateSourceInstance:で「関連(Phones)」が完了します。最初はこの流れが見抜けず?だったのですが、わかってみればなるほど納得の挙動です。ところで、このマイグレーションでは、AddressBook->PhoneBookの他にPhoneBook->AddressBookの関連もあります。で、残るPhoneBook -> AddressBookの関連は当然createRelationshipsForDestinationInstance:entityMapping:manager:error:で実装するのだと思っていましたがさにあらず。このメソッド内でNSLog()してみたら、

NSLog(@"%@ %@ :Tel %@", 
  [[dInstance valueForKey:@"addressBook"] valueForKey:@"firstName"],
  [[dInstance valueForKey:@"addressBook"] valueForKey:@"lastName"],
  [dInstance valueForKey:@"phoneNumber"]);

すでにPhoneBookのaddressBookには関連するインスタンスがセットされていました。

MyMigration[675:a0f] createRelationshipsForDestinationInstance
MyMigration[675:a0f] 浦島 太郎 :Tel 090-1234-5678
MyMigration[675:a0f] createRelationshipsForDestinationInstance
MyMigration[675:a0f] 桃 太郎 :Tel 123-4567-7891
MyMigration[675:a0f] createRelationshipsForDestinationInstance
MyMigration[675:a0f] 桃 太郎 :Tel 090-111-9999

どうも、関連が双方向の場合どちらか一方(この場合はAddressBookのphones)を作成するタイミングでディスティネーション側の関連プロパティも作成されるようです。考えてみればphonesをセット時点でPhoneBook側のaddressBookも確定できているワケですからこれは楽ちん。

以上、簡単な例ですがやってみたらなんとかできました。Core Dataに関してはあらかじめマイグレーションの手段を確認しておくことで開発はずいぶん楽になるはずです。マイグレーションの制限がアプリの機能アップを妨げることのないよう練習しておくのは悪く気がします。(モデル設計がより重要なんですけどね...)

サンプルソースはhttp://github.com/hippos/MyMigrationにあります。

この記事のトラックバックURL:

http://hippos-lab.com/blog/trackback/368

Comments