ページ

2010年5月21日金曜日

CoreData - リレーションシップ(5) マスター・ディティールとカスケード削除

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

(前回)Cocoaの日々: CoreData - リレーションシップ(4) タグを使った検索

前回までは多対多のリレーションシップでを扱ったが、今回は一般的なマスター・ディティール(1対多)を扱ってみる(順番でいけばこっちの方を先に試すべきだったかもしれない)。

まず BlogComment というモデルを追加し、BlogEntry と関連づける。

設定。

BlogComment.entry は、必須(非オプション)、削除ルール=カスケード、としておく。

コメント登録コード:

-(void)addComment
{
NSManagedObjectContext* moc = [self managedObjectContext];
// (1) fetch from BlogEntry
NSFetchRequest* request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:@"BlogEntry"
  inManagedObjectContext:managedObjectContext]];

NSError* error = nil;
NSArray* entries = [moc executeFetchRequest:request error:&error];

[request release];

// (2) add comments
BlogComment* comment1 = [NSEntityDescription insertNewObjectForEntityForName:@"BlogComment"
inManagedObjectContext:moc];
comment1.comment = @"RDBとはモデリング手法が若干異なる。";
comment1.created = [NSDate date];

BlogComment* comment2 = [NSEntityDescription insertNewObjectForEntityForName:@"BlogComment"
  inManagedObjectContext:moc];
comment2.comment = @"徐々にイメージがわいてきた";
comment2.created = [NSDate date];

BlogEntry* entry1 = [entries objectAtIndex:0];
[entry1 addCommentsObject:comment1];
[entry1 addCommentsObject:comment2];


BlogComment* comment3 = [NSEntityDescription insertNewObjectForEntityForName:@"BlogComment"
  inManagedObjectContext:moc];
comment3.comment = @"楽しみ";
comment3.created = [NSDate date];
BlogEntry* entry2 = [entries objectAtIndex:1];
[entry2 addCommentsObject:comment3];

[moc save:&error];
if (error) {
NSLog(@"INSERT ERROR: %@", error);
} else {
NSLog(@"INSERTED: BlogEntry");
}
}



実行結果。うまく紐づけられたようだ。
[16508:10b] title=CoreData のリレーションしっぷについて
[16508:10b] *tag*: Mac 
[16508:10b] *tag*: iPad
[16508:10b] *tag*: iPhone
[16508:10b] *comment*: 徐々にイメージがわいてきた
[16508:10b] *comment*: RDBとはモデリング手法が若干異なる。
---
[16508:10b] title=iPad 5/28発売
[16508:10b] *tag*: iPad
[16508:10b] *comment*: 楽しみ

データの状態はこんな感じ。
BlogEntry
  |--"CoreData のリレーションしっぷについて"
  |   |-- tags: "Mac" "iPad" "iPhone"
  |   |-- comments: "徐々にイメージがわいてきた", "RDBとはモデリング手法が若干異なる。"
  |
  |--"iPad 5/28発売"
  |   |-- tags: "iPad"
  |   |-- comments: "楽しみ"


次は BlogEntryを削除してみる。削除ルール=カスケードが働くかどうか。

-(void)deleteFirstEntry
{
NSManagedObjectContext* moc = [self managedObjectContext];
// (1) fetch from BlogEntry
NSLog(@"----- executeFetchRequest ------------------------------------------");
NSFetchRequest* request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:@"BlogEntry"
  inManagedObjectContext:managedObjectContext]];
NSError* error = nil;
NSArray* entries = [moc executeFetchRequest:request error:&error];
[request release];
BlogEntry* entry1 = [entries objectAtIndex:0];
NSLog(@"----- delete an entry ------------------------------------------");
[moc deleteObject:entry1];
[moc save:&error];
if (error) {
NSLog(@"DELETE ERROR: %@", error);
} else {
NSLog(@"DELETED: BlogEntry");
}
}


実行するとエラー発生:


[16738:10b] ----- delete an entry ------------------------------------------
[16738:10b] CoreData: sql: SELECT 0, t0.Z_PK FROM ZBLOGCOMMENT t0 WHERE  t0.ZENTRY = ? 
[16738:10b] CoreData: annotation: sql connection fetch time: 0.0046s
[16738:10b] CoreData: annotation: total fetch execution time: 0.0090s for 2 rows.
[16738:10b] CoreData: annotation: to-many relationship fault "comments" for objectID 0x17d8f0 <x-coredata://812B263A-7054-4EFF-8987-00EBD700187E/BlogEntry/p1> fulfilled from database.  Got 2 rows
[16738:10b] CoreData: sql: SELECT 0, t0.Z_PK FROM Z_2TAGS t1 JOIN ZTAG t0 ON t0.Z_PK = t1.Z_3TAGS WHERE t1.Z_2ENTRIES = ? 
[16738:10b] CoreData: annotation: sql connection fetch time: 0.0028s
[16738:10b] CoreData: annotation: total fetch execution time: 0.0046s for 3 rows.
[16738:10b] CoreData: annotation: to-many relationship fault "tags" for objectID 0x17d8f0 <x-coredata://812B263A-7054-4EFF-8987-00EBD700187E/BlogEntry/p1> fulfilled from database.  Got 3 rows
oreDataRelationship[16738:10b] CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZCOMMENT, t0.ZCREATED, t0.ZENTRY 
   :
(関連を辿ったフェッチが続く)
   :


[16738:10b]DELETE ERROR: Error Domain=NSCocoaErrorDomain Code=1560 UserInfo=0x186840 "Multiple validation errors occurred."


それにしても削除するのに大量にフェッチが実行している。

SQLを追ってみる。

1. ZBLOGCOMMENT から当該entryのコメントの Z_PK を取得

SELECT 0, t0.Z_PK FROM ZBLOGCOMMENT t0 WHERE  t0.ZENTRY = ? 


2. Z_2TAG(ZBLOGENTRYとZTAGの中間テーブル)と ZTAGを join して、タグの Z_PK を取得

SELECT 0, t0.Z_PK FROM Z_2TAGS t1 JOIN ZTAG t0 ON t0.Z_PK = t1.Z_3TAGS WHERE t1.Z_2ENTRIES = ? 


3. ZBLOGCOMMENT からデータを取得(恐らく 1.の結果を利用)

SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZCOMMENT, t0.ZCREATED, t0.ZENTRY FROM ZBLOGCOMMENT t0 WHERE  t0.Z_PK = ? 


SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZCOMMENT, t0.ZCREATED, t0.ZENTRY FROM ZBLOGCOMMENT t0 WHERE  t0.Z_PK = ? 


※コメントは2つあるので2回 SELECTが飛んでいる

4. TAGからデータを取得(恐らく 2.の結果を利用)

SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME FROM ZTAG t0 WHERE  t0.Z_PK = ? 



5.中間テーブルを経由して、4.で取得したタグに紐づく ZBLOGENTRY の Z_PK を取得。

SELECT 0, t0.Z_PK FROM Z_2TAGS t1 JOIN ZBLOGENTRY t0 ON t0.Z_PK = t1.Z_2ENTRIES WHERE t1.Z_3TAGS = ? 



6. 再び ZTAGのデータを取得(削除対象のエントリが3つのタグを持っているので、その2つめ)

SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME FROM ZTAG t0 WHERE  t0.Z_PK = ? 



7. そのタグに紐づくZBLOGENTRYの Z_PK を取得
SELECT 0, t0.Z_PK FROM Z_2TAGS t1 JOIN ZBLOGENTRY t0 ON t0.Z_PK = t1.Z_2ENTRIES WHERE t1.Z_3TAGS = ? 



8. ZTAGのデータを取得(削除対象のエントリが3つのタグを持っているので、その3つめ)
SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME FROM ZTAG t0 WHERE  t0.Z_PK = ? 


9. そのタグに紐づくZBLOGENTRYの Z_PK を取得
SELECT 0, t0.Z_PK FROM Z_2TAGS t1 JOIN ZBLOGENTRY t0 ON t0.Z_PK = t1.Z_2ENTRIES WHERE t1.Z_3TAGS = ? 



10. エラー
DELETE ERROR: Error Domain=NSCocoaErrorDomain Code=1560 UserInfo=0x185680 "Multiple validation errors occurred."



なるほど。オブジェクトの依存関係を全て辿ってチェックをいれているようだ。

恐らくモデルの設定がまずいのだろう。今一度見直す。

BlogEntry の関連、comments と tags の削除ルールが「無効にする」になっている。もしかしてこれに引っかかっているのだろうか。comments, tags ともに削除ルールを「アクションなし」に変更してみた。




実行してみよう。

----- delete an entry ------------------------------------------
2010-05-21 14:59:15.895 CoreDataRelationship[16860:10b] CoreData: sql: BEGIN EXCLUSIVE
2010-05-21 14:59:15.920 CoreDataRelationship[16860:10b] CoreData: sql: DELETE FROM ZBLOGENTRY WHERE Z_PK = ? AND Z_OPT = ?
2010-05-21 14:59:15.922 CoreDataRelationship[16860:10b] CoreData: sql: DELETE FROM Z_2TAGS WHERE Z_2ENTRIES = 1
2010-05-21 14:59:15.924 CoreDataRelationship[16860:10b] CoreData: sql: COMMIT
2010-05-21 14:59:15.936 CoreDataRelationship[16860:10b] DELETED: BlogEntry


DELETEが発行された。ちゃんと ZBLOGENTRY と関連する Z_2TAGS(中間テーブル)のレコードが削除されている。ただ ZBLOGCOMMENT が無い?
DBを除いてみると:
SELECT * FROM ZBLOGCOMMENT;
1|1|1|1|296114355.733354|徐々にイメージがわいてきた
1|2|1|1|296114355.733213|RDBとはモデリング手法が若干異なる。
1|3|1|2|296114355.734608|楽しみ

残っている。

もしかして削除ルールの設定が逆か。BlogEntry.comments の削除ルールを「カスケード」に設定し、BlogComment.entry の削除ルールを「アクションなし」に設定するのか。
やってみる。



これでどうか?

2010-05-21 15:03:14.932 CoreDataRelationship[16880:10b] ----- delete an entry ------------------------------------------
[16880:10b] CoreData: sql: BEGIN EXCLUSIVE
[16880:10b] CoreData: sql: DELETE FROM ZBLOGCOMMENT WHERE Z_PK = ? AND Z_OPT = ?
[16880:10b] CoreData: sql: DELETE FROM ZBLOGCOMMENT WHERE Z_PK = ? AND Z_OPT = ?
[16880:10b] CoreData: sql: DELETE FROM ZBLOGENTRY WHERE Z_PK = ? AND Z_OPT = ?
[16880:10b] CoreData: sql: DELETE FROM Z_2TAGS WHERE Z_2ENTRIES = 1
[16880:10b] CoreData: sql: COMMIT


ZBLOGCOMMENT 対象の DELETE が出た。


なお削除ルールに「アクションなし」に設定するとビルド時に Warning が出る。


削除ルールの説明はここに書いてある。
Core Data Programming Guide: Relationships and Fetched Properties
この中の「Relationship Delete Rules」

Deny(拒否)、Cascade、No Action はいいとして、Nullify(無効にする)の定義を読むと、でデストネーション先を null にするとある。ディスとネーション先がオプション(非必須)の場合選択できる。

Tag.entries は今は「アクションなし」にしてみたが「無効にする」がいいのかもしれない。
次回、タグを削除して比較してみる。


ソース
CoreDataRelationship at 2010-05-21 from xcatsan's SampleCode - GitHub

- - - -
コーディングしていて思ったのだが、ORマッパーによくある findAll とか findBy〜 みたいなメソッドが欲しくなる。ひょっとして用意されている?