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という面倒な部分をラップしてくれるなどの恩賜が受けられる。

Post Navigation