Tag Archives: Coredata

#define MR_ENABLE_ACTIVE_RECORD_LOGGING 0

のように定義するとMagicalRecordのログを無効化出来るのですが、CocoaPods経由で入れると順番の依存などの問題によりログが無効にできない場合があります。 

そういう場合は、CocoaPodsでインストールする際に以下のような感じでPodfileにhookスクリプトを書くと、 Pods側のheaderに#define出来ます。

元ネタはios – Cocoapods: turning MagicalRecord logging off – Stack Overflowです。

CocoaPodsのHooksについて以下も参考にするといいかもしれません。

CoreDataを扱うライブラリであるMagicalRecordをよく使いますが、
MagicalRecord では保存するデータベースファイル(デフォルトはアプリ名.sqlite)の名前指定するメソッドしか用意されていません。

具体的には、Setting up the Core Data Stackにかかれているようにsetup*経由で初期化処理をする際に、ファイル名を指定することが出来ます。

+ (void) setupCoreDataStackWithStoreNamed:(NSString *)storeName;

等ですね。

この時、デフォルトでは Library/Application\ Support/アプリ名/アプリ名.sqlite にsqliteファイルが保存されます。

アプリのディレクトリ
├── Documents
├── Library
│   ├── Application\ Support
│   │   └── アプリ名
│   ├── Caches
│   └── Preferences
├── アプリ名.app
└── tmp

しかし、すでに Documents/アプリ名.sqlite というようにDocumentsディレクトリ以下にsqliteファイルが有った場合はそちらが使われます。

MagicalRecord内部ではどのようになっているのかを見てみます。

setupメソッドで渡したstoreNameは、+ (NSURL *) MR_urlForStoreName:(NSString *)storeFileNameを使いNSURLとして取得されています。
+ (NSURL *) MR_urlForStoreName:(NSString *)storeFileName では、NSDocumentDirectoryとNSApplicationSupportDirectory(Application\ Support)の両方をみています。

どちらかに該当するsqliteファイルがあったらそれを利用するようになっているため、
Document/ にsqliteがあったらそちらが使われますし、 Application\ Support/にsqliteがあった場合はこちらが使われます。

+ (NSURL *) MR_urlForStoreName:(NSString *)storeFileName はPublicメソッドなので、MagicalRecordを使っていて保存されているsqliteファイルのパスを取得したい場合は、
直接、NSApplicationSupportDirectoryなどを見ないで、+ (NSURL *) MR_urlForStoreName:(NSString *)storeFileNameを経由するのが無難でしょう。

最初に述べたように、setupメソッドには任意のNSURLを指定するメソッドは用意されていません。
(現時点では、[MagicalRecord setupCoreDataStackWithStore:storeURL]; のようにNSURLを渡すと、内部ではNSURLかを判定してる ため意図通りに動きますが、将来性は保証されてません)

そのためLibrary/Application\ Support/アプリ名/アプリ名.sqlite か Documents/アプリ名.sqlite 以外にsqliteファイルをおいている場合は、
その場所にファイルをコピーするなどの処理が必要になるでしょう。

その他

MagicalRecordの中の人がCore Data Recipes by Saul Mora [Leanpub PDF/iPad/Kindle]という書籍を書くことを検討しているみたいです。
興味がある人は書籍が完成したかどうかの通知に登録しておくといいです。 

CoreDataと違い、直接SQLiteを扱うためのライブラリであるFMDBを扱ったテストのセットアップ例を書いてみました。

テストフレームワークにKiwiを使っていますが、どのテストフレームワークもsetup/teardownは持ってるので同じことができます。

SQLManagerSpec.m がテストファイルです。

#define DATA_BASE_FILE_PATH @"/tmp/tmp.sqlite"

@interface SQLManagerSpec : KWSpec

@end

@implementation SQLManagerSpec

+ (void)buildExampleGroups {
    describe(@"SQLManager", ^{
        __block FMDatabase *fmDatabase;
        beforeEach(^{
            fmDatabase = [self database];
            [fmDatabase open];
        });
        afterEach(^{
            [fmDatabase close];
            [self cleanup];
        });
    });
}

+ (FMDatabase *)database {
    FMDatabase *fmDatabase = [FMDatabase databaseWithPath:DATA_BASE_FILE_PATH];
    return fmDatabase;
}

+ (void)cleanup {
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if (![fileManager fileExistsAtPath:DATA_BASE_FILE_PATH]) {
        return;
    }
    NSError *error = nil;
    [fileManager removeItemAtPath:DATA_BASE_FILE_PATH error:&error];
    if (error) {
        NSLog(@"%@", [error localizedDescription]);
    }
}
@end

テストの中身は省略してますが、beforeEach/afterEachが毎回テストの前後で実行される処理ですね。

beforeEachでデータベースファイルを開いて、afterEachでデータベースを閉じてデータベースを削除しています。
CoreDataのようにメモリストアを使う方法がわかればそちらでもいい気がします。(FMDB初めて使ったのでよくわからなかった)

 DATA_BASE_FILE_PATHを絶対パスにしているのは、ファイル名だけだと多分ロジックテストの制約に引っかかったからだと思います。

CoreDataだと以下の記事を参考にすれば、同じようなことができます

MagicalRecordを使った場合は以下のような感じです。

データベース周りは比較的テストを書きやすい部分なので積極的にテストを書いていくといいと思います。

テストをコマンドラインから実行する場合はxctoolがお勧めです。
azu/FMDB_test_setup · GitHub にもConfiguration (.xctool-args)が置いてあるので、xctool testとコマンドを叩けばCLIでテストが実行出来ます。
(まだCocoaPodsを完全にはサポートしてないので、ちょっと挙動おかしいですが Twitter / CocoaPods: ℹ xctool doesn’t support … )

 

いつもmogeneratorを使ってCoreDataのモデルクラスを生成して利用している。
mogeneratorはPersonというNameのEntityがあった場合には、

  • Person.(h|m)
  • _Person.(h|m)

と4種類のファイルを生成する。

_Person.(h|m)には、基本的にいじる必要のない便利メソッドやモデルクラスっぽいプロパティ等が定義される。 Entityを更新等して再生成した時に上書きされるので、手動で変更する必要はない。


    + (id)insertInManagedObjectContext:(NSManagedObjectContext*)moc_;
    + (NSString*)entityName;
    + (NSEntityDescription*)entityInManagedObjectContext:(NSManagedObjectContext*)moc_;
    - (FoodModelID*)objectID;

Person.(h|m)の方はデフォルトは_Personのサブクラスとなっていて、必要に応じた拡張を書くクラスとなっている。

本題

自分の場合は、いつもEntityに identifier というUUIDを入れておく属性を作っている。

Skitched 20121003 174316

Entity Attribute

この identifier には 毎回CFUUIDCreateで作った値を入れているが、それを自動的に行うようにするために、Person.m-(void)awakeFromInsert を実装する。

awakeFromInsert は Entity作成時に自動的に呼ばれるメソッドなので、 Person.mのawakeFromInsert内で、次のようにself.identifierにUUIDを入れるような処理を書いておく。


- (void)awakeFromInsert {
    [super awakeFromInsert];
    CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
    NSString *uuidStr = (__bridge_transfer NSString *) CFUUIDCreateString(kCFAllocatorDefault, uuid);
    CFRelease(uuid);
    self.identifier = uuidStr;
}

こうすることで、Entityがインスタンス化された時に自動的にidentifierがセットされた状態で手に入るようになる。
identifierを設定するメリットは後からそのインスタンスを引くときに便利になるため、基本的にあった方が楽になる。
(デフォルトであってもいい気がするけど、何ももっといい方法が用意されているのかな)

 

 

CoreDataをそのまま利用する際は、シングルトンのマネージャー的なクラスを定義して、
そのマネージャー経由で managedObjectContextの取得やsaveを行うようにしている場合が多いと思います。
今回は、既にそのようなマネージャー経由で作っていたアプリをMagicalRecordをベースにしたものに書き換えるという趣旨です。 
MagicalRecordではNSManagedObjectContextのカテゴリ等でcontextの取得やsaveの保存ができるので、それを利用した形にしていきます。

例として、DCCoreDataというシングルトンのマネージャークラス(CoreDataプロジェクト作成時にやっておきたいこと | iPhoneアプリで稼げるのか) をMagicalRecordをベースに書き換えていきます。

まずはデータベースの保存先(方法)をMagicalRecordに

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {…}

persistentStoreCoordinatorではstoreURLを作って、NSPersistentStoreCoordinatorを返すような感じです。

#define FILENAME @"database"

- (NSURL *)applicationDocumentsDirectory {
    return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask]
                            lastObject];
}

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    if (persistentStoreCoordinator_ != nil){
        return persistentStoreCoordinator_;
    }

    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:
        [NSString stringWithFormat:@"%@.sqlite", FILENAME]];
    NSError *error = nil;
    persistentStoreCoordinator_ = [[NSPersistentStoreCoordinator alloc]
                                                                 initWithManagedObjectModel:[self managedObjectModel]];
    if (![persistentStoreCoordinator_
        addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]){
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    return persistentStoreCoordinator_;
}

これは、メソッド丸ごと削除して、[MagicalRecordHelpers setupCoreDataStackWithStoreNamed:]を使うようにします。適当にsetupCoreDataというメソッドにしておきます。

- (void)setupCoreData {
    NSString *fileName = [NSString stringWithFormat:@"%@.sqlite", FILENAME];
    [MagicalRecordHelpers setupCoreDataStackWithStoreNamed:fileName];
}

次にmanagedObjectContextの変更。

- (NSManagedObjectContext *)managedObjectContext{ … }

の変更

- (NSManagedObjectContext *)managedObjectContext {

    if (managedObjectContext_ != nil){
        return managedObjectContext_;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil){
        managedObjectContext_ = [[NSManagedObjectContext alloc] init];
        [managedObjectContext_ setPersistentStoreCoordinator:coordinator];
    }
    return managedObjectContext_;
}

となっていたのをMR_defaultContextにして使用するコンテキストをまとめる。

- (NSManagedObjectContext *)managedObjectContext {
    return [NSManagedObjectContext MR_defaultContext];
}

元々のmanagedObjectContexではpersistentStoreCoordinatorを使っていましたが、MagicalRecordHelpersのsetupを使って初期化されていれば、[NSManagedObjectContext MR_defaultContext]でコンテキストを取得できるので、persistentStoreCoordinatorは必要なくなります。

マイグレーション時は使うかもしれないけど、それも後述する[MagicalRecordHelpers setupCoreDataStackWithAutoMigratingSqliteStoreNamed:]に置き換えることができるので不要です。

次は

- (NSManagedObjectModel *)managedObjectModel {…}

の変更。

こちらも同様に

- (NSManagedObjectModel *)managedObjectModel {

    if (managedObjectModel_ != nil){
        return managedObjectModel_;
    }
    managedObjectModel_ = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];
    return managedObjectModel_;
}

だったのをMR_defaultManagedObjectModelを使うように変更する。

- (NSManagedObjectModel *)managedObjectModel {
    return [NSManagedObjectModel MR_defaultManagedObjectModel];
}

次にマイグレーション周りにコードをMagicalRecordベースに変更していきます。

これは自動生成されないので、データベースの構造が変わってマイグレーションが必要になってたプロジェクトには似たようなものがあるかもしれないです。

に書かれてるような、マイグレーションが必要かどうかのメソッドです。

- (BOOL)isRequiredMigration {
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:
        [NSString stringWithFormat:@"%@.sqlite", FILENAME]];
    NSError *error = nil;

    NSDictionary *sourceMetaData = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
        URL:storeURL
        error:&error];
    if (sourceMetaData == nil){
        return NO;
    } else if (error){
        NSLog(@"Checking migration was failed (%@, %@)", error, [error userInfo]);
        abort();
    }
    BOOL isCompatible = [self.managedObjectModel isConfiguration:nil compatibleWithStoreMetadata:sourceMetaData];

    return !isCompatible;
}

同様に、マイグレーションを行うメソッドも用意していて、doMigrationでマイグレーション処理を走らせるようにしていました。

- (NSPersistentStoreCoordinator *)doMigration {
    NSLog(@"--- doMigration ---");
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:
        [NSString stringWithFormat:@"%@.sqlite", FILENAME]];
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                                              [NSNumber numberWithBool:YES],
                                              NSMigratePersistentStoresAutomaticallyOption,
                                              [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,
                                              nil];
    NSError *error = nil;
    persistentStoreCoordinator_ = [[NSPersistentStoreCoordinator alloc]
                                                                 initWithManagedObjectModel:[self managedObjectModel]];
    if (![persistentStoreCoordinator_
        addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]){
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    return persistentStoreCoordinator_;
}

isRequiredMigration相当の処理はMagicalRecordには用意されてない?みたいなので、これはそのままにしました。
あえて変えるならstoreURL部分をsetupメソッドと同じ方法で取得するようにします。

- (BOOL)isRequiredMigration {
    NSURL *storeURL = [NSPersistentStore MR_urlForStoreName:[NSString stringWithFormat:@"%@.sqlite", FILENAME]];
    NSError *error = nil;
    NSDictionary *sourceMetaData = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
        URL:storeURL
        error:&error];
    if (sourceMetaData == nil){
        return NO;
    } else if (error){
        NSLog(@"Checking migration was failed (%@, %@)", error, [error userInfo]);
        abort();
    }
    BOOL isCompatible = [self.managedObjectModel isConfiguration:nil compatibleWithStoreMetadata:sourceMetaData];

    return !isCompatible;
}

doMigrationの方は中身的には[NSPersistentStoreCoordinator MR_autoMigrationOptions]を行なってから,

+ (NSDictionary *) MR_autoMigrationOptions;
{
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
                             [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,
                             nil];
    return options;
}

[MagicalRecordHelpers setupCoreDataStackWithStoreNamed:]で初期化するという感じだと思います。(マイグレーションのoptionsをつけたpersistentStoreCoordinatorみたいな感じです)

つまり[MagicalRecordHelpers setupCoreDataStackWithAutoMigratingSqliteStoreNamed:]するのと殆ど同じ感じになるので、doMigrationは次のように書き換えてみました。

- (void)setupWithMigration {
    NSString *fileName = [NSString stringWithFormat:@"%@.sqlite", FILENAME];
    [MagicalRecordHelpers setupCoreDataStackWithAutoMigratingSqliteStoreNamed:fileName];
}

実際にはマイグレーションをするというよりはsetupをするという感じになってるので、setupWithMigrationとメソッド名も変更しました。

こうすると2つのsetupメソッドができたので、isRequiredMigrationを使えば、次のようにどちらのsetupを行うかを分岐できるようになります(DCCoreDataというのは上記のメソッドを持つシングルトンクラス)

// CoreDataの初期化
if ([[DCCoreData sharedManager] isRequiredMigration]){
    [[DCCoreData sharedManager] setupWithMigration];
} else {
    [[DCCoreData sharedManager] setupCoreData];
}

これでsetup系は書き換えできので、後は保存等も書き換えていきます。 saveContextはそのままでも問題無いですが

- (void)saveContext {
    NSError *error = nil;
    NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
    if (managedObjectContext != nil){
        if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]){
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }
}

次のようにMR_saveを使って書き換えできます。

- (void)saveContext {
    [self.managedObjectContext MR_save];
}

削除用にdeleteObject:というものを用意してましたが

- (void)deleteObject:(NSManagedObject *)object {
    NSManagedObjectContext *context = self.managedObjectContext;
    [context deleteObject:[context objectWithID:object.objectID]];
    [self saveContext];
}

こちらも同様にMagicalRecordで書き換えてみると、以下のようにできます。(対して変わらない…)

- (void)deleteObject:(NSManagedObject *)object {
    NSManagedObjectContext *context = self.managedObjectContext;
    [[context objectWithID:object.objectID] MR_deleteEntity];
    [self saveContext];
}

deleteObject:とかはちょっとやりすぎで無意味な感じがしますが、ここまでをまとめるとDCCoreDataは次のような感じになりました。

最後に

それぞれを並べてる見ると、変更前のDCCoreData

変更後のDCCoreDataは以下のようになりました。
記事を書いた後にも少し変更したので、ちょっと解説とは違った部分があるかもしれません。

MagicalRecordのsetup*メソッドは二度呼びだそうとするとアプリが落ちるため、一応doesOnceSetUpでチェックを入れるようにしています。

既存にCoreDataを直接使っていたものからMagicalRecordに移行するメリットは、
CoreDataをUnitTestとかしたい – yaakaito’s diary のようにCoreDataをテストするためには少し準備がいるのが(まあサブクラスとかにしておけばあんまり気にならないけど)、

- (void)setUp;
{
    [MagicalRecord setDefaultModelWithClass:[self class]];
    [MagicalRecord setupCoreDataStackWithInMemoryStore];
}

- (void)tearDown;
{
    [MagicalRecord cleanUp];
}

という感じだけで済むようになる。

また、テスト用のデータ作成/操作 も少しシンプルに書けるようになる。(データベース部分やモデル等はテストしやすい場所なので積極的に書ける気がする)

後は、MagicalRecordの機能として単純なfetchなどはNSPredicateを使わないで書ける(普通にNSPredicateを使って取得もできる)事や、
マルチスレッドとCoreDataという面倒な部分をラップしてくれるなどの恩賜が受けられる。