ページ

2010年5月31日月曜日

Objective-C カテゴリでプロパティ

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

例えばこんなクラスがあるとする。

Customer.h

@interface Customer :  NSManagedObject  
{
}

@property (nonatomic, retain) NSString * address1;
@property (nonatomic, retain) NSString * postcode;
@property (nonatomic, retain) NSString * lastname;
@property (nonatomic, retain) NSString * address2;
@property (nonatomic, retain) NSString * firstname;
@property (nonatomic, retain) NSDate * birthdate;
@property (nonatomic, retain) NSString * firstnameKana;
@property (nonatomic, retain) NSString * lastnameKana;

@end



birthdate(誕生日)から現時点での年齢を計算したいのだが、これをプロパティとして取得できるようにしたい。これをカテゴリで実装する。

こんな感じ。

Customer+Extension.h

#import "Customer.h"

@interface Customer (Extension)

@property (assign, nonatomic, readonly) NSInteger age;

@end


追加メソッドをプロパティとして実装できる。

@implementation Customer (Extension)

- (NSInteger)ageForBirthday_:(NSDate *)dateOfBirth {
  :
}

- (NSInteger)age
{
return [self ageForBirthday_:self.birthdate];
}



これを使う場合はこんな感じ。

#import "Customer+Extension.h"



cell.detailTextLabel.text =
[NSString stringWithFormat:@"%d", customer.age];


参考)
Calculate age in objective-c | I ticked the wrong box

Cocoaの日々: CoreData - Xcodeでモデルクラスを自動生成する

2010年5月30日日曜日

今週のCocoa情報(5/30) - 今週気になった Cocoaプログラミング情報の紹介

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

How to get macos Log-in User Name in objective-c - Stack Overflow

Q)ログイン中のユーザ名を取得するには?
A)NSUserName() を使う

Foundation Functions Reference

他にも

NSFullUserName
NSHomeDirectory
NSHomeDirectoryForUser

なんてものがある。


Break on EXC_BAD_ACCESS in XCode? - Stack Overflow

EXC_BAD_ACCESS 解決に NSZombieEnabled を使う件。
CocoaDev: NSZombieEnabled



Cocoa Singleton conventions - Stack Overflow

Q)シングルトンパターンを使う場合のメソッド名は default〜 と shared〜 のどちらが良いか。

(例)
NSSingletonObject *so = [NSSingletonObject defaultSingleton];

  VS

NSSingletonObject *so = [NSSingletonObject sharedSingleton];

A)
shared〜 :このメソッド以外(お約束として)インスタンス生成ができない場合(真の?シングルトン)
default〜:自分でも作成可能だが、クラスがデフォルトインスタンスを提供する場合

なるほど。

参考)
Cocoa Fundamentals Guide: Cocoa Design Patterns - Singleton



how to rename or change file name case on the same location in objective-c - Stack Overflow

Q)ファイル名の大文字小文字を変換したい
 (例)abc.txt => (ABC.txt or Abc.txt or abc.TXT or abc.Txt)

- (BOOL)movePath:(NSString *)source toPath:(NSString *)destination handler:(id)handler
は、この用途では使えない。

A)ヒント↓
Possible Bug in NSFileManager -moveItemAtPath:toPath:error: | Cocoabuilder

systemコール "rename" を使ったらどうか、とのこと。
-fileSystemRepresentation で C文字列が取得できるのでそれを使う。



OCUnitの使い方(Cocoa Application 編) ([SM gakusyuu];)

環境の用意からテストケースの作成までの日本語解説。図入りで分かりやすい。






Cocoa Fundamentals Guide: Cocoa Design Patterns

一つ前の情報で紹介したものだが、こんなものがあったのか。

2010年5月29日土曜日

CoreData - テーブルの件数を取得する(SQL確認)

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

以前、別のブログで CoreDataでテーブルの件数を取得する記事を書いた。

Cocoa Touch の日々: CoreData - テーブルの件数を取得する

発行される SQLを調べてなかったので確認してみた。

SELECT COUNT(*) FROM ZSTYLETAG t0
※iPhoneSDK3.1.3にて


無駄なSQLは出ていないようだ(当然か)。

2010年5月28日金曜日

CoreData - SQLite の LIMIT

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

-[NSFetchRequest setFetchBatchSize:] で指定したサイズ値がフェッチ時に発行される SQLの LIMIT値となる。

例えば

[fetchRequest setFetchBatchSize:20];
とするとSQLはこうなる。
SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZMODIFIEDDATE, t0.ZFEE, t0.ZMEMO, t0.ZCREATEDDATE, t0.ZOPERATEDDATE, t0.ZCUSTOMER FROM ZKARTE t0 WHERE  t0.Z_PK IN  (?,?,?)  ORDER BY t0.ZOPERATEDDATE DESC LIMIT 20

3件ヒットするケースで、サイズを1とすると
[fetchRequest setFetchBatchSize:1];

こうなる。
sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZMODIFIEDDATE, t0.ZFEE, t0.ZMEMO, t0.ZCREATEDDATE, t0.ZOPERATEDDATE, t0.ZCUSTOMER FROM ZKARTE t0 WHERE  t0.Z_PK IN  (?)  ORDER BY t0.ZOPERATEDDATE DESC LIMIT 1

sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZMODIFIEDDATE, t0.ZFEE, t0.ZMEMO, t0.ZCREATEDDATE, t0.ZOPERATEDDATE, t0.ZCUSTOMER FROM ZKARTE t0 WHERE  t0.Z_PK IN  (?)  ORDER BY t0.ZOPERATEDDATE DESC LIMIT 1

sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZMODIFIEDDATE, t0.ZFEE, t0.ZMEMO, t0.ZCREATEDDATE, t0.ZOPERATEDDATE, t0.ZCUSTOMER FROM ZKARTE t0 WHERE  t0.Z_PK IN  (?)  ORDER BY t0.ZOPERATEDDATE DESC LIMIT 1

LIMIT 1 のSQLが件数分(3件)発行される。

※iPhone OS 3.1.3 用コードで確認

2010年5月27日木曜日

CoreData - 検索見本(NSManagedObjectを条件に検索)

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

NSPredicate を使う。
例えば、Customer(マスタ)<−−−>> Karte(ディティール)というモデルがあり、特定の Customer に紐づく Karteの一覧を取得する場合。


// Edit Predicade
NSPredicate* predicate =
  [NSPredicate predicateWithFormat:@"customer == %@", customer];
[request setPredicate:predicate];


オブジェクトが条件の場合でも上記のように等号条件として書ける。

SQLite のログはこう。
SELECT 0, t0.Z_PK FROM ZKARTE t0 WHERE  t0.ZCUSTOMER = ? ORDER BY t0.ZOPERATEDDATE DESC

SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZMODIFIEDDATE, t0.ZFEE, t0.ZMEMO, t0.ZCREATEDDATE, t0.ZOPERATEDDATE, t0.ZCUSTOMER FROM ZKARTE t0 WHERE  t0.Z_PK IN  (?,?,?)  ORDER BY t0.ZOPERATEDDATE DESC LIMIT 20

最初に Customer を条件にして合致する Karteの PK(Primary Key)を取得している。そして、取得した PK を SELECT ...IN( ) の条件として Karteのデータを得る。

# 合致する Karte が1万件あったら、IN (1万件の PK)となるのだろうか。
(追記)通常、LIMIT 制限するのでそんなことはおこらない。

※iPhone OS 3.1.3 用コードで確認

2010年5月26日水曜日

CoreData - マイグレーション

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

モデルの修正
CoreDataのマイグレーションを試してみた。通常モデルに修正を加えると、直前に作成したSQLiteDBは使えず実行時エラーが出る。この場合、SQLiteDBを一旦削除する必要がある。当然データの引き継ぎはできない。

エラーログ:








[16769:a0f] Error Domain=NSCocoaErrorDomain Code=134100 UserInfo=0x116760 "The managed object model version used to open the persistent store is incompatible with the one that was used to create the persistent store."
[16769:a0f] This NSPersistentStoreCoordinator has no persistent stores.  It cannot perform a save operation.


CoreDataでのデータマイグレーション
CoreDataにはマイグレーションの仕組みが用意されていて、これを利用することができる。

Core Data Model Versioning and Data Migration Programming Guide: Introduction to Core Data Model Versioning and Data Migration Programming Guide

属性値の追加など簡単な変更については自動マイグレーションが使えるようだ。

Core Data Model Versioning and Data Migration Programming Guide: Lightweight Migration

自動マイグレーションは下記が参考になる。
Core Dataの自動マイグレーション | hippos-lab::blog

試してみよう。



自動マイグレーション

1. モデルバージョンを追加する。
CoreDataRelationship_DataModel 2.xcdatamodel が追加された。



2. モデルを変更する。
今回は新しいバージョンの方に属性 published (NSData)を追加する。


3. Mapping Model を作成する。
名前は map-01-02.xcmappingmodel としてみた。
ソース(旧)とデスティネーション(新)のモデルをそれぞれ指定する。
4. Mapping Model ファイルを開く。
「差分を表示」を使うと新旧モデルの比較ができる。下図は published が追加されたことを表している。
5. 属性マッピングを設定する
published はオプションなのでそのままでも問題ないのだが、試しに created の値を使うようにしてみた。


6. マイグレーションコードを追加する。

NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];

[persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
 configuration:nil
   URL:url
       options:options
 error:&error];


options: にマイグレーション用オプションを追加する。

7. 現在のバージョンを設定する。
新バージョンを選択後にメニューから設定する。
チェックがつく。


8. 実行
エラーが出なくなった。SQLite の中をみてみる。
sqlite> .schema ZBLOGENTRY
CREATE TABLE ZBLOGENTRY ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZPUBLISHED TIMESTAMP, ZCREATED TIMESTAMP, ZTITLE VARCHAR, ZCONTENT VARCHAR );
ZPUBLISHED が追加されている。

データも ZCREATEDの値がコピーされているのが確認できた。
sqlite> select * from ZBLOGENTRY;
1|2|5|296460106.418831|296460106.418831|iPad 5/28発売|かくかくしかじか
2|2|8|296460106.418047|296460106.418047|CoreData のリレーションしっぷについて|かくかくしかじか



飛ばしバージョンアップ (1→3)
さらに続けてバージョン3を作る。その上で、バージョン1のデータがどうなるかをみてみよう。

まずバージョン3のモデルを作る。
BlogEntryへ visible(BOOL)を追加してみた。

Mappin Model を作る。ソースはバージョン2、デスティネーションはバージョン3にした。
新属性 visible には YES (==1)を当てた。
実行してみよう。まずはバージョン2→3の動作をみる。
sqlite> .schema ZBLOGENTRY
CREATE TABLE ZBLOGENTRY ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZVISIBLE INTEGER, ZPUBLISHED TIMESTAMP, ZCREATED TIMESTAMP, ZTITLE VARCHAR, ZCONTENT VARCHAR );



sqlite> select * from ZBLOGENTRY;
1|2|4|1|296460106.418831|296460106.418831|iPad 5/28発売|かくかくしかじか
2|2|6|1|296460106.418047|296460106.418047|CoreData のリレーションしっぷについて|かくかくしかじか
visibleが追加されているのが確認できた。

さて次はいよいよ1→3のマイグレーションを試みる。とっておいたバージョン1のSQLIteDBファイルをおいて、現在のモデルのバージョンを3として実行する。


結果はOK。

おお賢い。おそらく 1→2, 2→3 と自動的にマイグレーションを適用しているのだろう。


sqlite> .schema ZBLOGENTRY
CREATE TABLE ZBLOGENTRY ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZVISIBLE INTEGER, ZPUBLISHED TIMESTAMP, ZCREATED TIMESTAMP, ZTITLE VARCHAR, ZCONTENT VARCHAR );


sqlite> select * from ZBLOGENTRY;
1|2|4|1|296460106.418831|296460106.418831|iPad 5/28発売|かくかくしかじか
2|2|6|1|296460106.418047|296460106.418047|CoreData のリレーションしっぷについて|かくかくしかじか

気がついた点

バックアップファイルが自動生成される
SQLite のDBファイルがマイグレーション直前にコピーされ、バックアップがとられるようだ。


CoreDataRelationship.db
CoreDataRelationship~.db      ←バックアップ




NSInferMappingModelAutomaticallyOption
-[NSPersistentStoreCoordinator addPersistentStoreWithType:configuration:URL:options:error:] で渡すオプション NSInferMappingModelAutomaticallyOption は Mac OS v10.6 以上なので、v10.5 では使えない。

調べたところ iPhone は 3.0 以上で利用できるようだ。

2010年5月25日火曜日

CoreData - Xcodeでモデルクラスを自動生成する

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

Xcodeのモデルクラス自動生成機能を使った開発および運用方法など。

自動生成方法
モデルエディタでモデルを選択した後、メニューから「ファイル > 新規ファイル」を選ぶと管理クラスオブジェクトが選べるようになるのでこれを選択する。


生成されるクラス
下図のモデルのクラスを自動生成すると、関連(リレーションシップ)用のメソッドが追加される。



BlogEntry.h
@class Tag;
@class BlogComment;

@interface BlogEntry :  NSManagedObject  
{
}

@property (retain) NSDate * created;
@property (retain) NSString * title;
@property (retain) NSString * content;
@property (retain) NSSet* tags;
@property (retain) NSSet* comments;

@end

@interface BlogEntry (CoreDataGeneratedAccessors)
- (void)addTagsObject:(Tag *)value;
- (void)removeTagsObject:(Tag *)value;
- (void)addTags:(NSSet *)value;
- (void)removeTags:(NSSet *)value;

- (void)addCommentsObject:(BlogComment *)value;
- (void)removeCommentsObject:(BlogComment *)value;
- (void)addComments:(NSSet *)value;
- (void)removeComments:(NSSet *)value;

@end

BlogComment.h
@class BlogEntry;

@interface BlogComment :  NSManagedObject  
{
}

@property (retain) NSString * comment;
@property (retain) NSDate * created;
@property (retain) BlogEntry * entry;

@end

Tag.h
@class BlogEntry;

@interface Tag :  NSManagedObject  
{
}

@property (retain) NSString * name;
@property (retain) NSSet* entries;

@end

@interface Tag (CoreDataGeneratedAccessors)
- (void)addEntriesObject:(BlogEntry *)value;
- (void)removeEntriesObject:(BlogEntry *)value;
- (void)addEntries:(NSSet *)value;
- (void)removeEntries:(NSSet *)value;

@end


モデル変更時の対応
モデルの変更は、開発中のみならずシステム(アプリ)の運用・保守中にも必ず起こる。独自メソッドを自動生成クラスへ追加すると、その度にコードを追加しなければならなくなるので、これを避ける為にカテゴリを使う。

BlogEntryExtension.h

@interface BlogEntry (Extension) 
- (NSArray*)findAll;
@end






自動生成の自動化
mogenerator というソフトが公開されている。
nvie.com » Blog Archive » Automatically generate classes for your Core Data data model

これは未検証だが、使える内容ならビルドフェーズに組み込むと便利かもしれない。

2010年5月24日月曜日

CoreData - モデル見本

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

(前回)Cocoaの日々: CoreData - リレーションシップ(6) 削除ルール「アクションなし」「無効にする」の比較

前回までの検証結果を図にまとめてみた。





※SQLite のER図は Mac OS X 10.5での検証結果(10.6は未確認)

- - - -
間違い、改善点などあればぜひ教えていただきたい(コメントへどうぞ)。

2010年5月23日日曜日

今週のCocoa情報(5/23) - 今週気になった Cocoaプログラミング情報の紹介

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

GCDを試してみる - As Sloth As Possible
面白い。参考になった。


Best way to do interprocess communication on Mac OS X - Stack Overflow
MacOSXでの プロセス間通信の方法について。
・Control files
・Sockets
・Distributed objects
など。


get the position of icons on the Desktop (cocoa OSX) - Stack Overflow
デスクトップアイコンの位置を取得するには?
⇒ FSGetCatalogInfoBulk を使う(らしい)。

File Manager Reference - FSGetCatalogInfoBulk

Legacy: Finder Interface Reference



How to move a symlink to the trash? - Stack Overflow
シンボリックリンクをゴミ箱へ捨てるには?
⇒ FSPathMakeRefWithOptions() で FSRef を作り、それを FSMoveObjectToTrashSync() へ渡す。


Get text under mouse cursor in mac osx leopard/snow leopard - Stack Overflow
マウス下のテキストを取得するには?⇒かなり困難。OCRを使う?


Dictionary Services Programming Guide: Introduction
Dictionary Service なんてあったのか。知らなかった。標準の辞書.app に辞書コンテンツが追加できるようだ。


VideoDecodeAccelerationフレームワークが押し上げるMacのH.264再生環境 - builder by ZDNet Japan
Mac OS X 10.6.3 から「VideoDecodeAcceleration.framework」が追加されたらしい。次期 Snow Leopard用 Flashで使われる予定とのこと。


Face Detection API for Objective-C - Stack Overflow
顔認識の方法は? ⇒ OpenCV が使える。参考リンクなど。


Pointers to good reference on writing AppleScriptable Cocoa project? - Stack Overflow
Cocoa Scripting に関するリンク

2010年5月22日土曜日

CoreData - リレーションシップ(6) 削除ルール「アクションなし」「無効にする」の比較

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

(前回)Cocoaの日々: CoreData - リレーションシップ(5) マスター・ディティールとカスケード削除

タグを削除して削除ルールの違いを見てみる。

(参考)モデル




削除コード:

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


タグの削除

(1) 削除ルール「無効にする」

まず Tag.entriesの削除ルールを「無効にする」にして、タグを1つ削除してみる。

[17042:10b] CoreData: sql: BEGIN EXCLUSIVE
[17042:10b] CoreData: sql: DELETE FROM Z_2TAGS WHERE Z_2ENTRIES = 2 AND Z_3TAGS = 1
[17042:10b] CoreData: sql: DELETE FROM Z_2TAGS WHERE Z_2ENTRIES = 1 AND Z_3TAGS = 1
[17042:10b] CoreData: sql: DELETE FROM ZTAG WHERE Z_PK = ? AND Z_OPT = ?
[17042:10b] CoreData: sql: UPDATE ZBLOGENTRY SET Z_OPT = ?  WHERE Z_PK = 2 AND Z_OPT = 2
[17042:10b] CoreData: sql: UPDATE ZBLOGENTRY SET Z_OPT = ?  WHERE Z_PK = 1 AND Z_OPT = 2
[17042:10b] CoreData: sql: COMMIT


該当タグおよび中間テーブルがきちんと削除されている。その他、UPDATE が出てる。ZBLOGENTRY.Z_OPT が更新されているようだ。Z_OPT が何を指しているか不明。


(2) 削除ルール「アクションなし」

次は「アクションなし」

[17091:10b] CoreData: sql: BEGIN EXCLUSIVE
[17091:10b] CoreData: sql: DELETE FROM ZTAG WHERE Z_PK = ? AND Z_OPT = ?
[17091:10b] CoreData: sql: COMMIT


あー、ZTAGしか削除されていない。中間テーブル(Z_2TAGS)内のレコードは残ったままだ。
sqlite> select * from ztag;
3|2|2|iPhone
3|3|2|Mac
sqlite> select * from z_2tags;
1|1
1|2
1|3
2|1
↑ 1|1 と 2|1 が残ったまま。これはまずい。



というわけで、基本的にはデフォルトの「無効にする」が望ましい。


コメントの削除

ついでにコメントの削除も同様にみておく(リレーションが若干異なるので)。


(1) 削除ルール「無効にする」
[17156:10b] CoreData: sql: BEGIN EXCLUSIVE
[17156:10b] CoreData: sql: DELETE FROM ZBLOGCOMMENT WHERE Z_PK = ? AND Z_OPT = ?
[17156:10b] CoreData: sql: UPDATE ZBLOGENTRY SET Z_OPT = ?  WHERE Z_PK = 1 AND Z_OPT = 2
[17156:10b] CoreData: sql: COMMIT

該当するコメントが削除され、ZBLOGENTRYの Z_OPT が更新された。


(2) 削除ルール「アクションなし」

[17172:10b] CoreData: sql: BEGIN EXCLUSIVE
[17172:10b] CoreData: sql: DELETE FROM ZBLOGCOMMENT WHERE Z_PK = ? AND Z_OPT = ?
[17172:10b] CoreData: sql: COMMIT


コメントの削除のみ。


結論
紆余曲折あったが、削除ルールはデフォルトの「無効にする」が良い。

Z_OPT の意味が不明だが、これまでの挙動を見る限り「アクションなし」では意図しない動作になるので、デフォルトの「無効にする」が無難なようだ。「アクションなし」で Warning が出るのはその辺りの挙動について注意が必要ということだろう。


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

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〜 みたいなメソッドが欲しくなる。ひょっとして用意されている?

2010年5月20日木曜日

CoreData - リレーションシップ(4) タグを使った検索

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

(前回)Cocoaの日々: CoreData - リレーションシップ(3) 多対多のモデリング

今回はタグを使った検索を行ってみる。といってもエントリの検索と全く同じ。

- (void)fetchDataByTag
{
NSManagedObjectContext* moc = [self managedObjectContext];
NSFetchRequest* request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:@"Tag"
inManagedObjectContext:managedObjectContext]];
NSError* error = nil;
NSLog(@"----- executeFetchRequest ------------------------------------------");
NSArray* tags = [moc executeFetchRequest:request error:&error];
NSLog(@"----- listup entry ---------------------------------------------------");
for (Tag* tag in tags) {
NSLog(@"tag name=%@", tag.name);
for (BlogEntry* entry in tag.entries) {
NSLog(@"*entry*: %@", entry.title);
}
}
[request release];
}


結果
[16229:10b] ----- executeFetchRequest ------------------------------------------
[16229:10b] ----- listup entry ---------------------------------------------------
[16229:10b] tag name=Mac
[16229:10b] *entry*: CoreData のリレーションしっぷについて
[16229:10b] tag name=iPad
[16229:10b] *entry*: iPad 5/28発売
[16229:10b] *entry*: CoreData のリレーションしっぷについて
[16229:10b] tag name=iPhone
[16229:10b] *entry*: CoreData のリレーションしっぷについて

発行している SQLをチェックしてみる。

[16245:10b] CoreData: sql: pragma cache_size=1000
16245:10b] ----- executeFetchRequest ------------------------------------------
[16245:10b] CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME FROM ZTAG t0
[16245:10b] CoreData: annotation: sql connection fetch time: 0.0682s
[16245:10b] CoreData: annotation: total fetch execution time: 0.0752s for 3 rows.
[16245:10b] ----- listup entry ---------------------------------------------------
[16245:10b] tag name=Mac
[16245:10b] CoreData: sql: SELECT 0, t0.Z_PK FROM Z_1TAGS t1 JOIN ZBLOGENTRY t0 ON t0.Z_PK = t1.Z_1ENTRIES WHERE t1.Z_2TAGS = ? ...(1)
[16245:10b] CoreData: annotation: sql connection fetch time: 0.0054s
[16245:10b] CoreData: annotation: total fetch execution time: 0.0102s for 1 rows.
[16245:10b] CoreData: annotation: to-many relationship fault "entries" for objectID 0x1715a0 <x-coredata://C7F5193B-3731-4955-8F11-51453ABAAC99/Tag/p1> fulfilled from database.  Got 1 rows ...(2)
[16245:10b] CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZTITLE, t0.ZCONTENT, t0.ZCREATED FROM ZBLOGENTRY t0 WHERE  t0.Z_PK = ? ...(3)
[16245:10b] CoreData: annotation: sql connection fetch time: 0.0043s
[16245:10b] CoreData: annotation: total fetch execution time: 0.0079s for 1 rows.
[16245:10b] CoreData: annotation: fault fulfilled from database for : 0x173270 <x-coredata://C7F5193B-3731-4955-8F11-51453ABAAC99/BlogEntry/p2>[16245:10b] *entry*: CoreData のリレーションしっぷについて
 :
 :

ポイントとなる箇所の色を変えてある。

(1) まず中間テーブル Z_1TAGS からエントリのプライマリキーにあたるカラム値(Z_PK)を取得する。
(2) 続いて、Z_PKに対応するオブジェクトの状態が fault なので、DBへフェッチしにいく。
(3) (1)で取得した Z_PK(複数)の値を条件に ZBLOGENTRY からデータを取得し有効な BlogEntry のインスタンスを用意する。


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

- - - -
RDB しか経験が無いので、ついつい SQLite のスキーマを確認してしまった。ただそのおかげで徐々にモデリングのイメージがつかめてきた。