今回の内容
今回は、データベースに保存したオブジェクトのクラス定義に変更がある場合に、どの様に処理をすればいいのかを説明します。
プロジェクトを作成する
はじめてのNursery その2 実際に使ってみるを参考に新しいアプリケーションプロジェクトを作成し、Nurseryフレームワークを使用できる様にします。
ここでは名前をMigration-Exampleとします。
はじめてのNursery その3 NUCodingを実装したクラスを定義してNurseryで使うで作成したPersonクラスを流用するので、プロジェクトにそのPersonクラスを追加しておきます。
クラス定義の変更内容がインスタンス変数の追加/削除の場合でも、クラス名またはクラスの継承関係の変更の場合でも以下の様に行う処理はほぼ同じです。
- 変更するクラスの
defineCharacter:on:
メソッド内でクラスに対応するCharacterオブジェクトに新しいバージョン番号を指定 - NUCharacterTargetClassResolvingプロトコルを実装するクラスを定義し、そのインスタンスをGardenオブジェクトに
addCharacterTargetClassResolver:
メソッドで追加 -
initWithAliaser:
メソッドやencodeWithAliaser:
メソッドで、クラスと対応するCharacterオブジェクトのバージョン等で処理を分ける
PersonオブジェクトをNurseryデータベースに保存する
まずは流用したPersonクラスを保存する処理を実装します。
main.mの内容を次の様に変更します。
アプリケーションを実行して、起動したことを確認し、終了させます。
#import <Cocoa/Cocoa.h>
#import <Nursery/Nursery.h>
#import "Person.h"
static NUMainBranchNursery *nursery = nil;
static NUGarden *garden = nil;
void saveOrLoadPerson(void)
{
NSString *aFilepath = [NSHomeDirectory() stringByAppendingPathComponent:@"Migration-Example"]; //データベースを保存するファイルパスを取得
NUMainBranchNursery *aNursery = [NUMainBranchNursery nurseryWithContentsOfFile:aFilepath]; //ファイルパスを指定してデータベースオブジェクトを作成
NUGarden *aGarden = [aNursery makeGarden]; //データベースに保存するオブジェクトを管理するオブジェクトを作成
if (![aGarden root])
{
NSMutableArray *aPersons = [NSMutableArray array]; //Personオブジェクトを格納する配列
Person *aPerson = [Person new]; //データベースに保存するオブジェクトを作成
[aPerson setFirstName:@"first name a"]; //ファーストネームを設定
[aPerson setLastName:@"last name a"]; //ラストネームを設定
[aPersons addObject:aPerson];
[aGarden setRoot:aPersons]; //データベースに保存するルートオブジェクトにPersonオブジェクトを格納する配列を設定
[aGarden farmOut]; //メモリ上のオブジェクトをデータベースファイルに保存
}
nursery = aNursery;
garden = aGarden;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
saveOrLoadPerson();
}
return NSApplicationMain(argc, argv);
}
Personクラスの定義変更
先ほどのPersonクラスに次のようにparentsプロパティとchildrenプロパティを追加し、
親子関係を設定するためのいくつかのメソッドおよび、Personオブジェクトをエンコード/デコードするための処理を追加します。
Person.h
#import <Foundation/Foundation.h>
#import <Nursery/Nursery.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject <NUCoding> //NUCodingプロトコルを指定する
@property (nonatomic, nullable) NSString *firstName;
@property (nonatomic, nullable) NSString *lastName;
//プロパティ追加
@property (nonatomic, nullable, readonly) NSMutableArray *parents; //親の配列を追加
@property (nonatomic, nullable, readonly) NSMutableArray *children; //子の配列を追加
//データベースファイル内のオブジェクトとメモリ上のオブジェクトを結び付けるオブジェクト
@property (nonatomic, nullable, assign) NUBell *bell;
//メソッド追加
- (void)addParent:(Person *)aPerson;
- (void)addChild:(Person *)aPerson;
@end
NS_ASSUME_NONNULL_END
Person.m
#import "Person.h"
@implementation Person
//プロパティのアクセサメソッドのゲッターとセッターを定義するので、インスタンス変数を明示的に生成する
@synthesize firstName, lastName;
//インスタンス変数追加
@synthesize parents, children;
//データベース内でのクラス定義を行うメソッドdefineCharacter:on:を、Nurseryが自動的に呼び出す様にする
+ (BOOL)automaticallyEstablishCharacter
{
return YES;
}
//データベース内でのクラス定義を行う
+ (void)defineCharacter:(NUCharacter *)aCharacter on:(NUGarden *)aGarden
{
//Characterにバージョン指定を追加
[aCharacter setVersion:1]; //Characterのバージョンを1に指定する。デフォルトは0
[aCharacter addOOPIvarWithName:@"firstName"]; //OOP(オブジェクト)型のインスタンス変数としてファーストネームを追加する
[aCharacter addOOPIvarWithName:@"lastName"]; //OOP(オブジェクト)型のインスタンス変数としてラストネームを追加する
//Characterに親配列と子配列のインスタンス変数を追加
[aCharacter addOOPIvarWithName:@"parents"]; //OOP(オブジェクト)型のインスタンス変数として親の配列を追加する
[aCharacter addOOPIvarWithName:@"children"]; //OOP(オブジェクト)型のインスタンス変数としてこの配列を追加する
}
//メモリ上のオブジェクトのインスタンス変数をデータベースに保存するための処理
- (void)encodeWithAliaser:(NUAliaser *)anAliaser
{
[anAliaser encodeObject:firstName forKey:@"firstName"];
[anAliaser encodeObject:lastName forKey:@"lastName"];
//親配列と子配列のエンコードを追加
[anAliaser encodeObject:parents forKey:@"parents"]; //親の配列をエンコードする
[anAliaser encodeObject:children forKey:@"children"]; //子の配列をエンコードする
}
//データベース内のオブジェクトからメモリ上のオブジェクトにインスタンス変数を読み込む処理
- (instancetype)initWithAliaser:(NUAliaser *)anAliaser
{
//この時点では、実際のオブジェクトではなく、それと結び付けられたNUBellのインスタンスを読み込む
firstName = [anAliaser decodeObjectForKey:@"firstName"];
lastName = [anAliaser decodeObjectForKey:@"lastName"];
//以前エンコードされた時のバージョンが1であった場合
if ([[anAliaser character] version] == 1)
{
//親と子の配列をデコードする
parents = [anAliaser decodeObjectForKey:@"parents"];
children = [anAliaser decodeObjectForKey:@"children"];
}
return self;
}
- (NSString *)firstName
{
//実際に必要になった時点で、ファーストネームを取得して返す
return NUGetIvar(&firstName);
}
- (void)setFirstName:(NSString *)aFirstName
{
NUSetIvar(&firstName, aFirstName); //ファーストネームを設定する
// Personオブジェクトが変更されたことをNurseryに知らせる
[[self bell] markChanged];
}
- (NSString *)lastName
{
//実際に必要になった時点で、ラストネームを取得して返す
return NUGetIvar(&lastName);
}
- (void)setLastName:(NSString *)aLastName
{
NUSetIvar(&lastName, aLastName); //ラストネームを設定する
// Personオブジェクトが変更されたことをNurseryに知らせる
[[self bell] markChanged];
}
- (NSMutableArray *)parents
{
NUGetIvar(&parents);
//もし配列がなければ新しく作成する
if (!parents)
parents = [NSMutableArray new];
return parents;
}
- (NSMutableArray *)children
{
NUGetIvar(&children);
//もし配列がなければ新しく作成する
if (!children)
children = [NSMutableArray new];
return children;
}
- (void)addParent:(Person *)aPerson
{
[[self parents] addObject:aPerson];
//親の配列parentsを変更したのでNurseryにparentsが変更されたことを知らせる
[[[self bell] garden] markChangedObject:[self parents]];
}
- (void)addChild:(Person *)aPerson
{
[[self children] addObject:aPerson];
[aPerson addParent:self];
//子の配列childrenを変更したのでNurseryにchildrenが変更されたことを知らせる
[[[self bell] garden] markChangedObject:[self children]];
}
@end
NUCharacterTargetClassResolvingプロトコルを実装したクラスを定義する
次の様にNUCharacterTargetClassResolvingプロトコルを実装するクラスを定義します。
クラス名はClassResolverとします。
resolveTargetClassOrCoderForCharacter:onGarden:
メソッドの引数のCharacterオブジェクトが持つ情報をもとに、そのCharacterオブジェクトをどのクラスに対応付けるのかを指定します。
ClassResolver.h
#import <Nursery/Nursery.h>
NS_ASSUME_NONNULL_BEGIN
@interface ClassResolver : NSObject <NUCharacterTargetClassResolving>
@end
NS_ASSUME_NONNULL_END
ClassResolver.m
#import "ClassResolver.h"
#import "Person.h"
@implementation ClassResolver
- (BOOL)resolveTargetClassOrCoderForCharacter:(NUCharacter *)aCharacter onGarden:(NUGarden *)aGarden
{
//Characterオブジェクトの名前がPersonかつ、バージョンが0であった場合
if ([[aCharacter name] isEqualToString:@"Person"] && [aCharacter version] == 0)
{
//CharacterのターゲットクラスをPersonクラスに設定する
[aCharacter setTargetClass:[Person class]];
return YES;
}
return NO;
}
@end
main.mの変更
次の様にmain.mを変更します。
GardenオブジェクトにaddCharacterTargetClassResolver:メッセージを送り、先ほど定義したClassResolverのインスタンスを追加します。
#import <Cocoa/Cocoa.h>
#import <Nursery/Nursery.h>
#import "Person.h"
#import "ClassResolver.h"
static NUMainBranchNursery *nursery = nil;
static NUGarden *garden = nil;
void saveOrLoadPerson(void)
{
NSString *aFilepath = [NSHomeDirectory() stringByAppendingPathComponent:@"Migration-Example"]; //データベースを保存するファイルパスを取得
NUMainBranchNursery *aNursery = [NUMainBranchNursery nurseryWithContentsOfFile:aFilepath]; //ファイルパスを指定してデータベースオブジェクトを作成
NUGarden *aGarden = [aNursery makeGarden]; //データベースに保存するオブジェクトを管理するオブジェクトを作成
//Characterオブジェクトのターゲットクラスを解決するResolverをGardenオブジェクトに追加する
ClassResolver *aCharacterTargetClassResolver = [ClassResolver new];
[aGarden addCharacterTargetClassResolver:aCharacterTargetClassResolver];
if (![aGarden root])
{
NSMutableArray *aPersons = [NSMutableArray array]; //Personオブジェクトを格納する配列
Person *aPerson = [Person new]; //データベースに保存するオブジェクトを作成
[aPerson setFirstName:@"first name a"]; //ファーストネームを設定
[aPerson setLastName:@"last name a"]; //ラストネームを設定
[aPersons addObject:aPerson];
[aGarden setRoot:aPersons]; //データベースに保存するルートオブジェクトにPersonオブジェクトを格納する配列を設定
[aGarden farmOut]; //メモリ上のオブジェクトをデータベースファイルに保存
}
nursery = aNursery;
garden = aGarden;
}
void addFamilyAndSave(void)
{
//親子オブジェクトを作成
Person *aParentA = [Person new], *aParentB = [Person new], *aChildA = [Person new];
//名前を設定
[aParentA setFirstName:@"parent a"];
[aParentA setLastName:@"family a"];
[aParentB setFirstName:@"parent b"];
[aParentB setLastName:@"family a"];
[aChildA setFirstName:@"child a"];
[aChildA setLastName:@"family a"];
//親子関係を設定
[aParentA addChild:aChildA];
[aParentB addChild:aChildA];
//親子をpersons配列に追加
NSMutableArray *aPersons = [garden root];
[aPersons addObject:aParentA];
[aPersons addObject:aParentB];
[aPersons addObject:aChildA];
//persons配列を変更したので、Nurseryにpersonsの変更を知らせる
[garden markChangedObject:aPersons];
//保存
[garden farmOut];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
saveOrLoadPerson();
addFamilyAndSave();
}
return NSApplicationMain(argc, argv);
}
変更が終わった状態でアプリケーションを実行すると、saveOrLoadPerson()
関数でバージョン0のPersonクラスのインスタンスが読み込まれ、addFamilyAndSave()
関数でバージョン1のPersonクラスのインスタンスが作成され保存されます。
Nurseryではオブジェクトをデータベースに保存した時点のクラスに対応するCharacterオブジェクトも保存されています。データベース内には、バージョンが違う同じ名前のCharacterを保存できるため、例えデータベースへ保存後にクラス定義を変更したとしても、変更があったクラスのインスタンスを新しいクラス定義に合わせて、一度に全て変更する必要はありません。
次回
はじめてのNursery その4 一つのNurseryデータベースを複数のプロセスから使うで説明したNurseryが持つgradeの役割についてもう少し詳しく説明する予定です。