以前に『Objective-C 再入門 -1.0.2- coreData を使う』という記事を書いた。
そこでは、余計なことを気にしないようにコマンドラインツールで CoreData を使ったんだが、実務的にはこんな使い方はしない。大抵の場合、iOS プロジェクトで GUI 環境で使う場合が多いと思う。
そういうわけで、今回は GUI 環境で使うときの覚え書き。
諸々の事情で MacOS だけどさ。
最も簡単な Core Data サンプル
デフォルトに Person クラス(プロパティは name )を追加して AppDelegate の applicationDidFinishLaunching メソッドに以下のコードを実装。
_persistentContainer = [self persistentContainer];
NSManagedObjectContext *context = _persistentContainer.viewContext;
Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person"
inManagedObjectContext:context];
person.name = @"taro";
NSError *error = nil;
[context save:&error];
これで、永続化されている。
モデルからクラスを自動生成する場合、元になるクラスのコーディングは不要
見出しの通り。
モデルで Person というクラスを想定した場合、自動生成した時、Person+XXX というファイルが4つほどできるが、元の Person というクラスをあらかじめコーディングしておくのは不要。
というのは、現在の Xcode (Ver 14)とは動作が違うのか @property などでせこせこ Person クラスを作成してから、自動生成させる、みたいに書いてある記事があって、???となっていたから。
確かに Java の hibernate では、クラスのコーディングはコーダーの仕事だが、CoreData では xxx.xcdatamodeled 上でクラスを定義するので、そこから考えても不要。
なんか、おかしな記事だった。
今回は、One To と Many To も定義したが、自動生成すると、例えば、One To Many の One 側のクラスは

となる。
Many の側として Child クラスを想定したが、Person からしたら Child のインスタンスは複数取りえるのでアクセサの返値の型は
NSSet<Child *>
になるわけですね。
その記事、その説明もなんかおかしかったな。
CoreData に限らず、ORM の使い方をなにか誤解している人は多いので、リアルでもこういう人への対処は注意ですね。
あと、relationship の名前が、そのままメンバー変数の名前になる(こういう仕様は使ってみないとわからない)ので、モデル上での作業がかなり重要ということがわかります。
.xcdatamodeled でのクラス定義
話を戻して。
今回は、クラスを二つ用意して、その間に One To と Many To の関係性(relationship)を入れます。
わかってしまえば、作業は少ない。
One 側のクラス Person の定義は↓。

Many 側のクラス Child の定義は↓。

これらを定義したら、いつものように
menu -> Editor -> Create NSManagedObject subclass…
で、クラスを自動生成させます。
機能は何もありませんが、ビルドするだけなら、この時点でランさせてもエラーは出ないはずです。
問題となるのは、実際に永続化するときでしょう。
また、循環参照の問題とか起こる・・・のか?
永続化
もうちょっと上手い書き方もあるような気もするのだが、永続化自体はできた。
context を用意して
Child *child = [NSEntityDescription insertNewObjectForEntityForName:@"Child"
inManagedObjectContext:context];
child.name = @"taro";
Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person"
inManagedObjectContext:context];
person.name = @"akiba";
[person addChildrenObject:child];
として、CoreData が生成してくれたメソッド addChildrenObject で person に child を追加する。
save とすると、


と sqlite に永続化できた。
この時、ZCHILD_ZPERSON_INDEX に関係性の情報が格納されている、ということなんだろう。
しかし、循環参照は起きなかったが、どこで context を生成するとか、もうちょっと経験を積む必要がありそう。
永続化実体の sqlite ファイルの場所
ところで、永続化したとき、この情報を永続化している sqlite ファイルはデフォルトではどこにあるのだろうか?
実は、最初どこにあるのか不明で本当に永続化できているか確認のしようがなかった。
あたりをつけて探したら見つかったんだが、わかりにくいぞ(笑)。
アプリ自体の名前が CoreDataTest として、
bundle indentifier が com.sample.CoreDataTest2
モデルの名前が CoreDataTestDB
ユーザーの名前が hoge さんだったとすると
/Users/hoge/Library/Containers/CoreDataTest2/Data/Library/Application Support/CoreDataTest/CoreDataTestDB.sqllite
になる。長っ。
ココアアプリの場合、デフォで作成される persistentContainer をそのまま流用すれば、このファイルは自動で作ってくれる。説明が前後するが、前回のコマンドラインツールの場合は、(古い書き方のせいで)自分で作る必要があった。
fetchRequest
永続化はできたので、今度はこれら情報を取り出したい。
persistentContainer を定義した同一クラスで情報を取り出す適当なメソッドを作成し以下のように実装した。
NSManagedObjectContext *context = self.persistentContainer.viewContext;//or newBackgroundContext
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:NSStringFromClass([Child class])];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSArray *results = [context executeFetchRequest:fetchRequest error:nil];
NSUInteger i = 0;
for (Child *data in results) {
//do something.
NSLog(@"data: %lu name: %@", (unsigned long)i, data.name);
i++;
}
このメソッドを呼び出す(一番簡単には menu item のアクション実行先をこのメソッドにする)。
Target Output には、Child クラスの name が永続化された個数分、表示される。

できてますね。
Child クラスは Person クラスと One To Many の関係があるので
NSLog(@"data: %lu name: %@ Person name: %@", (unsigned long)i, data.name, data.person.name)
とすると Person の name も表示されます。
なんか ORM っぽくなってきました。
Person を読んでから、関係性の情報を使って Child の name を取得・表示する。
for (Person *data in results) {
//do something.
NSLog(@"data: %lu Person name: %@", (unsigned long)i, data.name);
NSArray *children = [data.children allObjects];
for(Child *child in children){
NSLog(@" Child name: %@", child.name);
}
i++;
}
NSSet で返されるので、一括して NSArray に変換しておくと後の処理が楽でしょうか。
NSTableView を組み込む
Xcode 上での TableView の操作に関しては、(Swift & iOS になってしまうが)
『CoreData と TableView の使い方』
が参考になるでしょうか。
Tips
本題とは直接関係ないが、気になったことなど。
レガシーコード
レガシーなプロジェクトでは NSPersistentContainer を使わないようだ。
こちらの 「iOS10 より前の・・・」項など参照。
sqlite ファイルを削除するときは思い切りよく
いまいち仕組みがわかってないのだが、XXX.sqlite を動作させると XXX.sqlite-shm と XXX.sqlite-wal というファイルも生成される。

通常使用なら気にしなくていいのだが、元の XXX.sqlite を削除した場合、ここで使っていた情報は生成された二つのファイルにも書き込まれているので、これらも削除しないとアプリが新規 sqlite ファイルをオープンできずエラーが出る。
テストの時にうっかりやってしまうと「え? ワイ何か重大なミス犯したかな」と一瞬固まるので(笑)、こういう場合は思い切りよく削除しましょう。
テスト環境だし。
予定
CRUD 操作・・あたり。
(続く)