iTunes U スタンフォード大学のiOSアプリ開発講義のLecure 13(Core Data)の講義メモです。
Core Data
Core Dataはストレージの基盤でSQLデータベースの上で成り立っている。
Xcodeのツールを使ってビジュアルにオブジェクトとデータベースをマッピングさせることが出来る。そしてそれがNSManagedObjectのサブクラスになる。
■Xcodeのビジュアルマップ(Data Model)で行われるマップ
Entity -> クラス
Attribute -> プロパティ
Relationship -> データベースの中で他のオブジェクトをポイントするプロパティ
・Attributesで設定したもの
Integer -> NSNumber
String -> NSString
Boolean -> NSNumber
Binary data -> NSData
Date -> NSDate
■コードからアクセスする方法
NSManagedObjectContextのインスタンスを作成してそのインスタンスにメッセージを送ることでアクセスする。
・インスタンスを作成する2つの方法
- UIMangedDocument(iOS 5の新クラス)を作成してそのmanagedObjectContextプロパティを使う
- プロジェクト作成時に"Use Core Data"にチェックを入れる
AppDelegateがmanagedObjectContextプロパティを持っているのでそれを使う
UIManagedDocument
UIManagedDocumentは、UIDocumentのサブクラスで、Core Dataデータベースを保持する枠組みである。
■UIManageDocumentの作成
[[UIManagedDocument alloc] initWithFileURL:(URL *)url];
このインスタンスを作成した状態ではまだファイル操作は一切行っていない状態。
・documentの作成
ファイルの存在チェック
存在していれば、documentをオープン
存在していなければ、作成
forSaveOperation:(UIDocumentSaveOperation)operation
competionHandler:(void (^)(BOOL success))completionHandler;
ファイルのオープンと作成は非同期に行われるので、blockで完了時の処理を引数として渡す。
<例>
self.document = [[UIManagedDocument alloc] initWithFileURL:(URL *)url];
if ([[NSFileManager defaultManager] fileExistsAtPath:[url path]]) {
[document openWithCompletionHandler:^(BOOL success) {
if (success) [self documentIsReady];
if (!success) NSLog(@“couldn’t open document at %@”, url);
}];
} else {
[document saveToURL:url forSaveOperation:UIDocumentSaveForCreating
completionHandler:^(BOOL success) {
if (success) [self documentIsReady];
if (!success) NSLog(@“couldn’t create document at %@”, url);
}];
}
{
if (self.document.documentState == UIDocumentStateNormal) {
NSManagedObjectContext *context =
self.document.managedObjectContext;
//Core Data contextを使って処理
}
}
NSNotificationCenter
NSNotificationは、delegate と違ってラジオ局のようにNSNotificationCenterがブロードキャストで複数の受信者に一斉に通知を送る。
使用コストが高いので乱用は避ける。
■使用方法
1. NSNotificationCenter に通知をもらうように登録
2. 通知を受け取った際のアクションをメソッドで実装
3. 使い終わったらNSNotificationCenterの登録を削除
<例>
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self
selector:@selector(contextChanged:)
name:NSManagedObjectContextObjectsDidChangeNotification
object:self.document.managedObjectContext];
}
- (void)viewWillDisappear:(BOOL)animated
{
[center removeObserver:self
name:NSManagedObjectContextObjectsDidChangeNotification
object:self.document.managedObjectContext];
[super viewWillDisappear:animated];
}
- (void)contextChanged:(NSNotification *)notification
{
/* notification.userInfoは次のキーを持ったNSDictionary
NSInsertedObjectsKey //挿入されたオブジェクトの配列
NSUpdatedObjectsKey //Attributesが更新されたオブジェクトの配列NSDeletedObjectsKey //削除されたオブジェクトの配列
*/
}
UIManagedDocument
■documentの保存
documentの保存は非同期で自動的に行われる。
明示的に行うには次のようにする。
[self.document saveToURL:self.document.fileURL
forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) {
if (!success) NSLog(@“failed to save document %@”, self.document.localizedName);
}];
<比較>
UIDocumentSaveForOverwritingはファイルが存在していなければエラー
UIDocumentSaveForCreatingはファイルが存在していればエラー
■documentのクローズ
UIManagedDocumentへのstrongポインタがなくなれば自動的に非同期で閉じられる。
明示的に行うには次のようにする。
[self.document closeWithCompletionHandler:^(BOOL success) {
if (!success) NSLog(@“failed to close document %@”, self.document.localizedName);
}];
■同一documentへの異なるポインタ
同一documentへ異なるポインタを持つと、異なるNSMagedObjectContextを持つ。しかし、一方で起こった変更は自動的には他方へ反映されない。そのため、一方を変更したら他方ではrefetchしないといけない。
コンフリクトが発生したら自分で解決する。
同時に書き込むケースはレアかもしれないが、複数の参照者がいるケースは多々ある。
Core Data
■データベースへオブジェクトを挿入
NSManagedObject *photo =
[NSEntityDescription insertNewObjectForEntityForName:@“Photo”
inManagedObjectContext:(NSManagedObjectContext *)context];
■MSmanagedObjectインスタンスのAttributesへアクセス
NSKeyValueObservingプロトコルの次のメソッドを使う
- (id)valueForKey:(NSString *)key;
- (void)setValue:(id)value forKey:(NSString *)key;
■Core Dataへのデータ書き込み時の注意点
変更した内容はメモリ上にあり、SQLデータベースには保存されていない。
UIManagedDocumentは自動保存機能があるが、多くの変更を行った時は明示的に保存するのが良い。
■よりエレガントなアクセス
valueForKey:/setValueForKey:はキー名を文字列で指定することになるので、これをさけるために、NSManagedObjectのサブクラスを作ってそのプロパティでアクセスする。
・NSManagedObjectのサブクラスの作成
XcodeでCore Dataのグラフィカルマップ図でEntitiesの中から作成したいエンティティを選んでメニューから Editor > Create NSmanagedObject Subclassを選択する。
※選択時に表示されるscalarプロパティのオプションは、数字をプリミティブな型にするかNSNumberオブジェクトにするかの選択であるが、NSDateは1970年からの時間に変換されてしまうので、このオプションは外しておくのが良い
→これで出来たファイルには、AttributesとRelationshipsがプロパティとして定義されている。しかし、各エンティティの関係は順番に評価されて行くので、最初に評価されたエンティティは他のエンティティへの正確なポインタがわからないので、MSManagedObjectへのポインタとしてプロパティ定義されてしまっている。
これを解決するために、再度メニューからサブクラスの作成を行う。
・実装ファイル
実装ファイルには@sysnthesizeに代わりに@dynamicが定義されているが、これは、@synthesizeを行っていないことでコンパイラにwarningを出させないようにしている。@synthesizeでsetter/getterを定義してないので、実行時には任意のメソッドを実行することが出来て、この場合はNSManagedObjectがvalueForKey:やSetValueForKey:を実行する。
・ドット表記でのアクセスの方法
Photo *photo = [NSEntityDescription
insertNewObjectForEntityForName:@“Photo” inManagedObj...];
NSString *myThumbnail = photo.thumbnailURL;
photo.thumbnailData = [FlickrFetcher
urlForPhoto:photoDictionary format:FlickrPhotoFormat...];
photo.whoTook = ...; //Photographerオブジェクトを示す
photo.whoTook.name = @“CS193p Instructor”; //連結して関係をたどれる
これで作成されたサブクラスに独自のメソッドを追加したい場合は、カテゴリーを使う。直接このサブクラスにコードを追加してしまうと、データベースに変更を加えて、サブクラスを再作成した時に無くなってしまい、再度追加することになる。
Objective-C カテゴリ
カテゴリーはサブクラスを作成せずにメソッドやプロパティをクラスに追加する
■構文
@interface Photo (AddOn)
- (UIImage *)image;
@property (readonly) BOOL isOld;
@end
■ファイル名の付け方
通常、クラス名+拡張の目的をファイル名とする。
■実装ファイルの記述例
@implementation Photo (AddOn)
-(UIImage*)image //imageisnotanattributeinthedatabase,but photoURL is
{
NSData *imageData = [NSData dataWithContentsOfURL:self.photoURL];
return [UIImage imageWithData:imageData];
}
-(BOOL)isOld //whetherthisphotowasuploadedmorethanadayago
{
return [self.uploadDate timeIntervalSinceNow] < -24*60*60;
}
@end
※カテゴリーにはインスタンスの追加は出来ないので、@synthesizeは記述出来ない
■注意点
既にクラスに存在しているものをカテゴリーで上書きしてしまう危険がある。
既存のメソッドを書き換えてしまうともとのクラスのコンセプトから逸脱するので、上書きは一切せず、追加だけにする。
■NSManagedObjectのサブクラスのカテゴリ記述例
・データ作成
@implementation Photo (Create)
+ (Photo *)photoWithFlickrData:(NSDictionary *)flickrData
inManagedObjectContext:(NSManagedObjectContext *)context
{
Photo *photo = ...; //DB内に既にFlickrのphotoデータがないかチェック
if (!photo) {
photo = [NSEntityDescription
insertNewObjectForEntityForName:@“Photo”
inManagedObjectContext:context];
//photoをFlickrのデータで初期化
//おそらく他のデータベースオブジェクトも作成する(Photographerなど)
}
return photo;
}
@end
・削除
[self.document.managedObjectContext deleteObject:photo];
※注意点:削除したあとにphotoへのstrongポインタを持ったままにしない
・削除の準備
@implementation Photo (Deletion)
- (void)prepareForDeletion
{
//リレーションのポインタは自動的にnilにセットされるので自分で実装する
//必要はない。
//しかし、例えばリンク先のPhotographerが削除しようとしている
//Photoの数を属性に持っていた場合は、ここでその数をデクリメントする。//(例えば、self.whoTook.photoCount--)
}
@end
Core Data
■データ検索
NSFetchRequestをNSManagedObjectContextの中で実行させることでデータの検索を行う。
1. 取得対象のEntityを指定
2. NSPredicateで取得対象データの制限を行う
※指定は任意で、デフォルトは全データ
3. NSSortDescriptorでデータの並び順を指定
4. 取得するオブジェクトの数(一度に取得する数、最大数)を指定
※指定は任意で、デフォルトは全データ
・NSFetchRequestの作成
//Entityの種類を指定
// ※一度のqueryで指定するのは1つのentityのみ
NSFetchRequest *request =
[NSFetchRequest fetchRequestWithEntityName:@“Photo”];
//一度に20づつデータ取得
request.fetchBatchSize = 20;
//最大取得数は100
request.fetchLimit = 100;
//ソート順の指定
request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
//取得対象データの制限を設定
request.predicate = ...;
・NSSortDescriptor
データはNSManagedObjectのNSArrayで返ってくるので、次のメソッドで並び順を指定する。
NSSortDescriptor *sortDescriptor =
[NSSortDescriptor
sortDescriptorWithKey:@“thumbnailURL”
ascending:YES
selector:@selector(localizedCaseInsensitiveCompare:)];
■NSPredicate
・フォーマット
NSString *serverName = @“flickr-5”;
NSPredicate *predicate =
[NSPredicate predicateWithFormat:@“thumbnailURL contains %@”,
serverName];
・条件指定例
@“name contains[c] %@”, (NSString *) //大文字小文字区別無しで部分文字列指定
@“viewed > %@”, (NSDate *) //日時条件指定
@“whoTook.name = %@”, (NSString *) //Photo検索 (Photographerのnameで)
@“any photos.title contains %@”, (NSString *) //Photographerからphotoを検索
・複合条件の指定
@“(name = %@) OR (title = %@)”
NSArray *array = [NSArray arrayWithObjects:predicate1, predicate2, nil];
NSPredicate *predicate =
[NSCompoundPredicate andPredicateWithSubpredicates:array];
■検索実装例
//Photographersの全部に対して
NSFetchRequest *request =
[NSFetchRequest fetchRequestWithEntityName:@“Photographer”];
// ...直近の24時間に写真を取った人
NSDate *yesterday =
[NSDate dateWithTimeIntervalSinceNow:-24*60*60];
request.predicate =
[NSPredicate predicateWithFormat:@“any photos.uploadDate > %@”,
yesterday];
// ... Photographerの名前でソートして
NSSortDescriptor *sortByName =
[NSSortDescriptor sortDescriptorWithKey:@“name” ascending:YES];
request.sortDescriptors = [NSArray arrayWithObject:sortByName];
//検索実行
NSManagedObjectContext *moc = self.document.managedObjectContext;
NSError *error;
NSArray *photographers =
[moc executeFetchRequest:request error:&error];
<前の記事 |
CS193p - Lecture 12 | - | CS193p - Lecture 14 | 次の記事> |
コメントをお書きください