at_yasu's blog

ロード的なことを

CoreData Migration ~ Lightweight Migration を使った方法

CoreData Migration - xcmappingmodel 編」と「CocoaBindingを使って画像管理ソフトを作成」の合わせ技です。

なお、「CoreData Migration - xcmappingmodel 編」はサンプルコードすらない旧聞の状態なので、新しいのでも問題ないという人はこちら推奨。

ちなみに私も勉強中なので、間違ってる事書いてたら突っ込みよろしく。日本語で書かれたのがホントに無いんだよね、何故か・・・



CoreDataを使っている場合EntityやPropertyなどのModelを変更する場合が、しょっちゅうあります。今回はそのModelを変更した際の対応方法。

今回は、前回「CocoaBindingを使って画像管理ソフトを作成」で作成したソフトを改良するという形で話を進めて行きます。


前回の失念点

まず、前回のアプリで「[appdelegate addImage:img forBook:b];」となってて動く訳ネェです。はい、ごめんなさい。appdelegateはそのままの意味でして、Interface builderでapplication delegate に接続しています。


addImage:forBook: は下記のようなメソッドです。

- (void) addImage:(NSImage*)image
          forBook:(Books*)object
{
    Images* img = [[Images alloc] initWithEntity:[[self.managedObjectModel
                                                   entitiesByName]
                                                  objectForKey:@"Images"]
                  insertIntoManagedObjectContext:self.managedObjectContext];
    
    img.image = [image TIFFRepresentation];
    img.book = object;
    [object addImagesObject:img];
    [img release];
}

改良点

まずwindow部分でNSCollectionView を、ImageKitのIKImageBrowserViewを使います。BindingはNSCollectionViewと同じ感じにします。

ソースコード

さて、IKImageBrowserViewは渡されたオブジェクトに imageUID メッセージを送り、その返り値を元に画像をキャッシュし、パフォーマンスを維持します。

ですので、Images.arrangeObjects で得たimageに imageUID メッセージを送っていることになりますので、Categoryで無理矢理実装してやりましょう。


Images+IKImageBrowserItem.h

#import <Foundation/Foundation.h>
#import <Quartz/Quartz.h>
#import "Images.h"

@interface Images (Images_IKImageBrowserItem)
- (NSString *) imageRepresentationType;
- (id) imageRepresentation;
- (NSString *)  imageUID;
@end

Images+IKImageBrowserItem.m

#import "Images+IKImageBrowserItem.h"

@implementation Images (Images_IKImageBrowserItem)
- (NSString *)imageUID
{
    return @"N/A";
}
- (NSString *)imageRepresentationType
{
    return IKImageBrowserNSDataRepresentationType;
}
- (id)imageRepresentation
{
    return self.image;
}
- (NSString *)imageTitle
{
    return @"N/A";
}
@end


さて、これで画像は下の画像のように表示されるはずです。


Model の変更

上の画像では画像はそれぞれ全部違いますが、実は上の実装をすると、imageUID が全部同じ値を返すので、全部同じ画像になります。

ですので、Images が個別のPropertyを持つようにしてやれば良いです。ついでに、中身はNSData型なのにimageとproperty名がなっている所を、dataと変更する事にします。

まず、.xcdatamodeldファイルを選択し、メニューの「設計」→「データモデル」→「モデルバージョンを追加」でファイルを複製します。そして、上で書いた変更をします。(下の図を参考にして下さい)

変更前

変更後


なお、Imagesのソースファイルをこの時点で作っておいて下さい。


Mapping

さて、モデルの変更が終われば、次は変更箇所をマッピングをするためのファイルを作ります。

メニューの「ファイル」→「新規作成」→「Resource」→「Mapping Model」→任意のファイル名を入力します。

次に、変更する前とした後のモデルを選択するように求められてきます。*1

「ソースモデルを設定」には変更前のモデル、「デスティネーションモデルを設定」には変更後のモデルを選択して下さい。


Migration

これで移行のマッピングとモデルは出来上がりました。後はソースコードでいつするかを書くだけです。するタイミングは、Storeファイルを読み込む直前が現実的に良いです。

必要な機能としては、移行する機能、移行しているか確認する機能、の二つがあります。


移行しているか確認するメソッド
- (BOOL) isMigrateToPersistentStore:(NSPersistentStoreCoordinator*)psc
                              toURL:(NSURL*)sourceStoreURL
{
    NSString *sourceStoreType = NSSQLiteStoreType;
    NSError *error = nil;
    
    NSDictionary *sourceMetadata = [NSPersistentStoreCoordinator
                            metadataForPersistentStoreOfType:sourceStoreType
                                                         URL:[NSURL URLWithString:sourceStoreURL]
                                                       error:&error];
    
    if (sourceMetadata == nil)
    {
        // deal with error
        NSAssert(NO, @"sourceStore is none");
        return NO;
    }
    
    NSString *configuration = nil;/* name of configuration, or nil */ ;
    NSManagedObjectModel *destinationModel = [psc managedObjectModel];
    BOOL pscCompatibile = [destinationModel
                           isConfiguration:configuration
                           compatibleWithStoreMetadata:sourceMetadata];
 
    return pscCompatibile;
}

ちなみにこれは完璧と言える状態ではありません、ので説明できません*2 :-p

移行メソッド

Light-Weightなので、10.6,iPhoneOS 3.0 以降で無いと動きません。1.5だと・・・勉強しておきます。

- (BOOL) migration:(NSURL*)storeurl
           persist:(NSPersistentStoreCoordinator*)persistentStore
{
    NSError *error;
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
                             [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
    
    NSPersistentStore* store = [persistentStore addPersistentStoreWithType:NSSQLiteStoreType
                                                             configuration:nil
                                                                       URL:storeurl
                                                                   options:options
                                                                     error:&error];
    
    if (error)
    {
        NSAssert(NO, @"addPersistentStore is nil");
        NSLog(@"error: %@", error);
        return NO;
    }
    return YES;
}
Storeをアップデート。

アップデート部分はこんな感じ。どこに書くかはわかりますよね?:-)

    NSPersistentStoreCoordinator *persistentStoreCoordinator = <# NSPersistentStoreCoordinator  #>;
    NSURL *url = <# NSURL: The storedata file url #>;
    // update
    if (![self isMigrateToPersistentStore:persistentStoreCoordinator
                                   toURL:url])
    {
        if ([self migration:url persist:persistentStoreCoordinator] == NO)
        {
            [persistentStoreCoordinator release];
            persistentStoreCoordinator = nil;
            return nil;
        }
        [persistentStoreCoordinator release], persistentStoreCoordinator = nil; // 使用済みなので一回破棄
        persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc]
                                      initWithManagedObjectModel: mom];
    }
ManagedObjectModel

さてさて、これでは何故かManagedObjectModelがModelファイルを読み込まなくなりました。というのも、modelファイルが複数になったので、Modelディレクトリーが出来たため(な気配。ちょっと自身ない)。ですので、modelディレクトリーを読み込ませれば良いわけです。ちなみに、Modelディレクトリーは.momdという拡張子が付いたディレクトリーです。

- (NSManagedObjectModel *)managedObjectModel {

    if (managedObjectModel) return managedObjectModel;
	NSString *managedDataModelPath = [[NSBundle mainBundle] pathForResource:@"ImagePending_DataModel"
                                                                     ofType:@"momd"];

    managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:
                          [NSURL URLWithString:managedDataModelPath]];
//    managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:[NSBundle allBundles]
//                                                      forStoreMetadata:nil]
//                          retain];
    
    return managedObjectModel;
}

仕上げ

さて話を上の方に戻しまして、imageUID を合理的に動くようにしましょう。

Images+IKImageBrowserItem.m // 修正後

#import "Images+IKImageBrowserItem.h"

@implementation Images (Images_IKImageBrowserItem)
+ (NSString *)stringWithUUID
{
	CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
	NSString * str = (NSString*)CFMakeCollectable(CFUUIDCreateString(
                        kCFAllocatorDefault, uuid));
	CFRelease(uuid);
	return [str autorelease];
}

- (NSString *)imageUID
{
    if (self.UniqID)
        return self.UniqID;
    self.UniqID = [Images stringWithUUID];
    return self.UniqID;
}
- (NSString *)imageRepresentationType
{
    return IKImageBrowserNSDataRepresentationType;
}
- (id)imageRepresentation
{
    return self.image;
}
- (NSString *)imageTitle
{
    return @"N/A";
}
@end


ビルドしてエラー箇所を修正したら、後は動かすだけです。

登録しているが増量が多いほど時間がかかりますが、気長に待つしか無いです。

終了すれば、上手く動く・・・はず。ちなみに私の事なので、今回も何処か抜けているかと思います。上手く動かない時はコメントに突っ込みよろしくです。

今でもよくわからん所

  • Versioning. どう使うの?
  • 10.5の頃とどう変わったの?
  • NSMigrationManager,NSEntityMigrationPolicy の存在

*1:下記の図参考

*2:というのも、configurationってどう言う使い方するのかさっぱりわからないんだ