[iOS][Mac] CoreData - マイグレーション[3] エラー

2010年12月8日水曜日 | Published in | 0 コメント

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

Core Data でマイグレーション設定を行い実行すると次のエラーが出て落ちた。
[14647:207] *** Terminating app due to uncaught exception 'NSInvalidArgumentException',
 reason: 'Can't merge models with two different entities named 'Customer''
*** Call stack at first throw:
(
0   CoreFoundation    0x0150dbe9 __exceptionPreprocess + 185
1   libobjc.A.dylib   0x016625c2 objc_exception_throw + 47
2   CoreData          0x0037a9b9 +[NSManagedObjectModel modelByMergingModels:] + 3865
3   CoreData          0x00378fcb +[NSManagedObjectModel mergedModelFromBundles:] + 507
  :

調べてみると次の記事が見つかった。
iPhone Development: Core Data Migration Problems?

どうも下記のコードが問題のようだ。
- (NSManagedObjectModel *)managedObjectModel
{
if (managedObjectModel_ != nil) {
        returnmanagedObjectModel_;
    }

    managedObjectModel_ = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];

    returnmanagedObjectModel_;
}
上記の -[NSManagedObjectModel mergedModelFromBundles:] が新旧両方のモデルを一緒に読み込もう(merge)としてエラーになっているようだ。実際にアプリケーションパッケージの中を覗いてみると次のようになっていた。

*.momd という名のフォルダが作られてその中に新旧モデルファイル(*.mom)とバージョン管理用のファイルが入っている。-[NSManagedObjectModel mergedModelFromBundles:] はアプリケーションバンドル内のモデルファイル(*.mom)をバージョン管理を無視して読み込む(merge)しようとするようだ。なおマイグレーションを行わない初期の状態ではこのフォルダは存在せず、最初のバージョンのモデルファイル(*.mom)が一つあるだけ。

解決方法はモデルファイルの検索を NSManagedObjectModel に任せないで *.momd フォルダの位置を明示的に指定すれば良い。こんな感じ。
- (NSManagedObjectModel *)managedObjectModel
{
if (managedObjectModel_ != nil) {
        returnmanagedObjectModel_;
    }

    NSString *path = [[NSBundle mainBundle] pathForResource:@"hairConcierge" ofType:@"momd"];
    NSURL *url = [NSURLfileURLWithPath:path];
    managedObjectModel_ = [[NSManagedObjectModelalloc] initWithContentsOfURL:url];

    returnmanagedObjectModel_;
}
上記はブログで紹介されていたのと同等のコードだが、少し改良して momdフォルダ名を書かないコードにしてみた(必要だったので)。こんな感じ。
- (NSManagedObjectModel *)managedObjectModel
{
if (managedObjectModel_ != nil) {
        returnmanagedObjectModel_;
    }

    NSArray* paths = [[NSBundlemainBundle] pathsForResourcesOfType:@"momd" inDirectory:nil];
    NSURL *url = [NSURLfileURLWithPath:[paths objectAtIndex:0]];
    managedObjectModel_ = [[NSManagedObjectModelalloc] initWithContentsOfURL:url];

    returnmanagedObjectModel_;
}

[iOS][Mac] CoreData - マイグレーション[2] 自動マイグレーションで FUNCTIONを使う

2010年12月7日火曜日 | Published in | 0 コメント

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

前回 NSEntityMigrationPolicy を使ったマイグレーションを紹介した。

[前回] Cocoaの日々: [iOS][Mac] CoreData - マイグレーション[1] NSEntityMigrationPolicy を使う

NSEntityMigrationPolicy を使うことで複雑なマイグレーションを行うことができる。前回は NSDate から月と日を抽出するコードを書いたが、これぐらいであれば自動マイグレーション(Lightweigt Migration)が使えないだろうか。ドキュメントを読み返していると FUNCTION が使えそうなのがわかったのでこれを試してみる。


サンプル


まずは結果から。前回のコードを書き換えて自動マイグレーションに切り替えた。マッピングモデルはこんな感じ。
前回と同じ。後で説明するが NSEntityMigrationPolicy を今回も使う。
day と month の値式に今回は FUNCTION を記述。
FUNCTION($entityPolicy, "monthOfDate:" , $source.timeStamp)
ドキュメントを見つけられなったので推測になるが FUNCTION は最初の引数にメッセージを投げる先のインスタンス、2番目にセレクタ(文字列)、3番目以降がセレクタの引数、といった仕様のようだ。指定したインスタンスへ指定した引数でメッセージを投げ、その結果が移行先の属性値として使われる。

$entityPolicy は「カスタムポリシー」で指定したクラスのインスタンスを指す。今回は NSEntityMigrationPolicy のサブクラス EventEntityMigrationPolicy を指定した。この $ で始まる予約語には次のものがある。
NSMigrationManagerKey: $manager
NSMigrationSourceObjectKey: $source
NSMigrationDestinationObjectKey: $destination
NSMigrationEntityMappingKey: $entityMapping
NSMigrationPropertyMappingKey: $propertyMapping
NSMigrationEntityPolicyKey: $entityPolicy
Core Data Model Versioning and Data Migration Programming Guide: Mapping Overview より転載)

EventEntityMigrationPolicy の実装はこう。
@implementation EventEntityMigrationPolicy

- (NSNumber*)monthOfDate:(NSDate*)date
{
 NSCalendar* calendar = [[[NSCalendar alloc]
    initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
 NSDateComponents* components = [calendar components:NSMonthCalendarUnit|
    NSDayCalendarUnit fromDate:date];
 return [NSNumber numberWithInteger:[components month]];
}
- (NSNumber*)dayOfDate:(NSDate*)date
{ 
 NSCalendar* calendar = [[[NSCalendar alloc]
    initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
 NSDateComponents* components = [calendar components:NSMonthCalendarUnit|
    NSDayCalendarUnit fromDate:date];
 return [NSNumber numberWithInteger:[components day]];
}

前回の create..メソッドはコメントアウトしてある。


備考


FUNCTION のターゲットオブジェクト


Event という名前の NSManagedObjectContext をサブクラスを用意して、そこに -dayOfDate: -monthOfDate: を実装したが、実行時にはエラーとなった。
e[8858:207] -[NSManagedObject monthOfDate:]: unrecognized selector sent to instance 0x4d87c10
[8858:207] *** Terminating app due to uncaught exception 'NSInvalidArgumentException',
 reason: '-[NSManagedObject monthOfDate:]: unrecognized selector sent to instance 0x4d87c10'

モデル定義でクラス指定しても駄目だった。
本来はここにマイグレーションコードが書けるといいのだが(NSEntityMigrationPolicyのインスタンスを用意する必要がなくなる)。


属性マッピングの値式に文字列を指定

属性マッピングの値式に文字列を書くときは ""で囲う。
◯ "Hello" × Hello ※ null扱いになる



ソースコード


GitHub からどうぞ。
xcatsan/iOS-Sample-Code at 2010-12-07 - GitHub

[iOS][Mac] CoreData - マイグレーション[1] NSEntityMigrationPolicy を使う

2010年12月6日月曜日 | Published in | 0 コメント

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

以前、CoreData のマイグレーションについて調べたことがある。
(旧) Cocoaの日々: CoreData - マイグレーション

これはマッピングモデルを定義するだけで簡単にできる、いわゆる「自動マイグレーション」を解説していた。
今回は NSEntityMigrationPolicy を使ったいわゆる「手動マイグレーション」について調べた。


マイグレーションの概要


Core Data ではエンティティ(テーブル)の定義を変更すると必ずマイグレーションを行う必要がある。例えば現在のアプリケーションをバージョンアップする際に属性 email2 を一つ追加する場合、マイグレーション設定を行わないと Core Data 利用時に例外が発生してアプリケーションが落ちてしまう。
(例) CoreDataMigSample[8394:207] Unresolved error
 Error Domain=NSCocoaErrorDomain Code=134100 "The operation couldn’t be completed. (Cocoa error 134100.)"
 UserInfo=0x581be80 {metadata={type = immutable dict, count = 7,
    :
}
, reason=The model used to open the store is incompatible with the one used to create the store}, {
    metadata =     {
    :

マイグレーションを行うには2つの方法がある。

 1. Lightweight Migration(いわゆる自動マイグレーション)
 2. NSEntityMigrationPolicy利用(いわゆる手動マイグレーション)



1. の方法は Xcode でマッピングモデルを用意するだけでマイグレーションの設定ができる。属性の追加など簡易なものであればこの方法で最も簡単にマイグレーションを実現できる。
(詳しくは「(旧) Cocoaの日々: CoreData - マイグレーション」まで)

2. の方法は NSEntityMigrationPolicy のサブクラスを作成してマイグレーションに必要な実装をコーディングする。今回紹介するのは 2. の方法。


NSEntityMigrationPolicy を利用したマイグレーション


マイグレーションプロセスは "Three-Stage Migration" と呼ばれる。Core Data 利用時にバージョンの差異が検出されるとこのマイグレーションプロセスが実行される。
1. Creation Stage(属性のマイグレーション)
2. Relationship Creation Stage(関連のマイグレーション)
3. Validation Stage(検証ステージ)

NSEntityMigrationPolicy のサブクラスを用意して設定しておくと、このマイグレーションプロセスの中で NSEntityMigrationPolicy の各メソッドが呼び出されていく。サブクラスで必要に応じてこれらのメソッドをオーバーライドしてマイグレーションコードを記述する。

NSEntityMigrationPolicy Class Reference

メソッドの呼び出し順序は次の通り。
[1] – beginEntityMapping:manager:error: 

== Creation Stage ==
[2] – createDestinationInstancesForSourceInstance:entityMapping:manager:error:
    ⇒ インスタンス(NSManagedObject)の数だけ繰り返し呼び出される
[3] – endInstanceCreationForEntityMapping:manager:error:

== Relationship creation stage ==
[4] – createRelationshipsForDestinationInstance:entityMapping:manager:error:
   ⇒ インスタンス(NSManagedObject)の数だけ繰り返し呼び出される
[5] – endRelationshipCreationForEntityMapping:manager:error:

== Validation stage ==
[6] – performCustomValidationForEntityMapping:manager:error:
==
[7] – endEntityMapping:manager:error:
マイグレーション対象のエンティティ毎に NSEntityMigrationPolicy のサブクラスを定義し、マイグレーションプロセスに適した実装を行って行くのがこの方法のスタイルとなる。

なおマイグレーションプロセス自体をカスタマイズすることができる。詳しくは下記を参考のこと。
Core Data Model Versioning and Data Migration Programming Guide: Customizing the Migration Process


サンプル


実際に NSEntityMigrationPolicy を使ったマイグレーションのサンプルプログラムを作ってみる。

まず Xcodeのテンプレートから "Navigation-based Application" を選びオプション "Use Core Data for storage" を付けて新規プロジェクトを作成する。ビルドすると[+]ボタンを押した時のタイムスタンプを Core Data へ格納してテーブル表示するアプリが動く。
エンティティはこんな感じ。
このエンティティへカラムを追加する。day, memo, month を追加してみよう。
day と month は timeStamp の月と日の値(整数)を設定する。

まず新しいバージョンのモデルを作成する。現在のモデルファイル(*.xcdatamodel)を選択してメニュー「設計」→「データモデル」→「モデルバージョンを追加」を選ぶ。すると新しいバージョンのモデルファイルが作成する。
このファイルを開き、先ほどのカラムを追加する。

次にマッピングモデルファイルを作成する。新規ファイル作成で "Mapping Model" を選ぶ。
新旧のモデルファイルを指定すると、マッピングモデルファイルが生成される。ここでは 1to2.xcmappingmodel という名前にしてみた。マッピングモデルファイルを開くと、エンティティ毎のマッピング設定が表示される。
カスタムポリシーに NSEntityMigrationPolicy のサブクラス名を設定する。今回は EventEntityMigrationPolicy とした。

次に EventEntityMigrationPolicy クラスを作る。

@interface EventEntityMigrationPolicy : NSEntityMigrationPolicy {

}@end

@implementation EventEntityMigrationPolicy

- (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)sInstance
entityMapping:(NSEntityMapping *)mapping manager:(NSMigrationManager *)manager error:(NSError **)error
{
 NSManagedObjectContext* context = [manager destinationContext];
 NSString *entityName = [mapping destinationEntityName];
 
 NSDate *timeStamp = [sInstance valueForKey:@"timeStamp"];
 
 NSCalendar* calendar = [[[NSCalendar alloc]
             initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
 NSDateComponents* components = [calendar components:NSMonthCalendarUnit|
             NSDayCalendarUnit fromDate:timeStamp];

 NSManagedObject* dInstance = [NSEntityDescription
             insertNewObjectForEntityForName:entityName inManagedObjectContext:context];

 [dInstance setValue:timeStamp forKey:@"timeStamp"];
 [dInstance setValue:[NSNumber numberWithInt:[components month]] forKey:@"month"];
 [dInstance setValue:[NSNumber numberWithInt:[components day]] forKey:@"day"];
 [dInstance setValue:@"Good morning!" forKey:@"memo"];
 
 return YES;
}
@end
sInstance にマイグレーション元のインスタンスが渡されるのでここから day と month を抽出してマイグレーション先のインスタンスへ設定する。

ポイントは次の通り。

1. 移行先のインスタンス(NSManagedObject)を生成する必要がある。
 Core Data 側でお膳立てはしてくれない。

2. すべての属性について移行の処理を実装する。
 移行処理を書かない属性は移行されない(当たり前だが)。
 (この処理はモデル情報を利用すれば定型化できる)

3. マッピングモデルで定義された属性のマッピングは参照されない。

マイグレーションプロセスは Core Data 側が管理するが、個々の具体的なマイグレーション処理は開発者が責任を持って全属性について実装する必要がある。



ソースコード


GitHub からどうぞ。
xcatsan/iOS-Sample-Code at 2010-12-06 - GitHub


備考


マイグレーションは1件毎にメソッドが呼び出されて処理される。この為、件数が多い場合は時間・メモリの面でパフォーマンスが問題になるケースが出てくる。この場合は標準のマイグレーションプロセスをカスタマイズして最適化することが可能。この方法については下記に解説がある。
Multiple Passes—Dealing With Large Datasets



参考情報


Core Data Model Versioning and Data Migration Programming Guide: Introduction to Core Data Model Versioning and Data Migration Programming Guide
公式リファレンス。基本的な情報はここでほとんど網羅されている。

Core Dataのマイグレーション(手動編) | hippos-lab::blog
NSEntityMigrationPolicy を使い単一テーブルから複数テーブルへ変換するリレーションシップを持つケースのマイグレーションの解説。わかりやすい。

CoreData - モデル、エンティティのクラス図

2010年12月5日日曜日 | Published in | 0 コメント

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

現在 Core Data のマイグレーションを調査中。その為の整理としてモデルとエンティティのクラス図を Xcodeのモデルエディタで作成した。

ふむふむ。

NSManagedObjectModel .... エンティティを管理する(RDBでいうDBの定義に近い)。バージョン情報もここで扱う。
NSEntityDescription .... エンティティの定義(RDBでいうテーブルの定義)
NSPropertyDescription ... プロパティの定義(RDBでいうカラムの定義)。属性やリレーションシップの派生クラスがある。


参考情報


Core Data Programming Guide: Core Data Basics

@synthesize default と Class Extensions Variables

2010年12月4日土曜日 | Published in | 0 コメント

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

LLVM と Clang を使うと次のことができるらしい。

・@synthesize の省略
・Class Extensions でインスタンス変数定義

元ネタはここから。
M Cubed Software - Blog


設定方法はターゲットのビルド設定を開き2つの設定を行う。

1つはコンパイラの指定。
LLVM compiler を選択する。

次はコンパイラオプションの指定。
 -Xclang -fobjc-nonfragile-abi2 を指定する。


すると下記のコードがコンパイルできるようになる。

SampleModel.h
@interface SampleModel : NSObject {

}

@end
SampleModel.m
#import "SampleModel.h"

@interface SampleModel()
{
 NSString* name;
}
@property (nonatomic, copy) NSString* name;
@end

@implementation SampleModel

-(NSString*)description
{
 return self.name;
}

@end

@synthsize を書かなくてもプロパティが使える。また Class Extensions(カテゴリ)でもインスタンス変数の定義が可能。これらは GCC でコンパイルするとエラーになる。

- - - -
LLVM を使うか GCC(デフォルト)を使うかはまだ意見が割れている感じ。LLVMの方がエラーメッセージが分かりやすい、コンパイル速度が速い、一方、GCCの方がトータルの速度が速い、安定している、といった意見(噂?)もあるようだ。



参考情報


Low Level Virtual Machine - Wikipedia

clang - Wikipedia

Nick Farina - Simplify your models with SMModelObject

この特性を生かしたモデルクラス SMModelObject の活用例。上記サイトから引用。

こんな感じで定義が書ける。dealloc もいらない(!)。
-description がオーラバーライドされていてXML風な表示

Tower - Git管理GUIツール

2010年12月3日金曜日 | Published in | 0 コメント

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

Towerベータ版を使い始めたが結構いい感じ。



画面は3ペインで、左にブランチ、右上にファイルリスト、右下に修正内容が表示される。
ブランチ、タグ、リモートリポジトリ、Stash を一覧して見ることができる。

これはCommit履歴を表示させたところ。



これ一つで Staging、Commit、Push までひと通りの操作が行える。

Xcodeを開いてファイルを修正していると、その修正を自動的に検出してリストアップしてくれる。

通常は作業の区切りでこれら確認して (add →) stage → commit していく。

Dockアイコンには未commitの修正ファイル数が表示される。

commit するとこれがクリアされる。これが作業終了の区切りとなってちょっと気分がいい。

部分的(chunk)に Stageすることもできるようだ。

ファイル操作のメニュー


Commit のメタ情報を条件に検索することもできる。
こんな感じ。

様々な Diff /Merge ツールに対応していて選ぶことができる。



複数のリポジトリが扱えるがウィンドウは1つしか開かない。1つのウィンドウを切り替えて使うスタイルになる。
これは将来複数ウィンドウ化を期待したい。



- - - - -
ファイル更新の自動検出のアイディアは秀逸。このおかげで特に工夫することなくXcodeと連携して使うことができる。これがかなりいい。現時点ではベータ版で無料だが Registration... というメニューがあることから正式版は有料になる可能性がある。よく出来ているので正式版が出たら購入するかもしれない。

なお現時点で Commit Message に日本語が入力できなかった。

NSArrayController を使った NSTableView で選択行の情報を取得する

2010年12月2日木曜日 | Published in | 1 コメント

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

NSArrayController でソート(SortDescriptors)を使っている場合の選択行の取得方法について。

-[NSTableViewDeleage tableView:shouldSelectRow:] で得た NSTableView上の選択位置 row を使って単純にモデル配列内の選択行を取り出す場合はこんな感じになる。
id obj = [modelArray objectAtIndex:row];
これは画面上でソートしている場合間違った行情報を取得することになる。
イメージ
NSTableView
 ↑
NSArrayController(ソートされている)
 ↓
モデル配列
画面上はソートした一覧が表示されていて、その並び順を元に選択位置 row を NSTableViewDelegateで通知する。つまり、
画面上:
 0:バナナ
 1:りんご
 2:苺

モデル配列:
 0:苺
 1:バナナ
 2:りんご
のようなケースが有りえて画面で「バナナ」を選択した場合 row=0 となるが、それをそのままモデル配列に当てはめると「苺」をさ指定しまう。


なので NSTableViewDelegate の受け側では渡された row を元に情報を取得する場合、オリジナルのモデル配列を使うのではなく画面上の並びに一致する NSArrayController のオブジェクトから情報を取得する必要がある。

NSArrayControlelr からの選択行の取り出しには -[arrangedObjects] を使う。これで取得した配列に対して objectAtIndex: を投げて目的の行情報を取得する。
id obj = [[arrayController arrangedObjects] objectAtIndex:row];

なお NSArrayController には選択行向けのメソッドが用意されている。
NSArrayController Class Reference
親クラスの NSObjectController にもある。
NSObjectController Class Reference

が、これらを使って意図通りの情報を取得することができなかった。使い方が悪いのか、設定が不足しているのか。もう少し調べる必要あり。

人気の投稿(過去 30日間)