今回の内容
前回までは、一つのプロセス内で、一つのNUMainBranchNurseryのインスタンスとNUGardenのインスタンスを作成して使用しました。
今回は、NUMainBranchNurseryのインスタンスを、NUNurseryNetServiceのインスタンスを使って、外部プロセスに公開します。そして複数の外部プロセスからNUGardenのインスタンスを作成して、それぞれのインスタンスからオブジェクトの読み込みと変更、保存を行います。
今回作成するアプリケーションでは、Counter(今回作成します)クラスのインスタンスをNUGardenのルートに設定し、そのカウントを増やす、または減らす操作をして、データベースに保存します。
サーバーアプリケーションとクライアントアプリケーションを作成する
サーバーアプリケーションを作成する
まず、はじめてのNursery その2 実際に使ってみる を参考にして、新しいアプリケーションプロジェクトを作成し、Nurseryフレームワークを使える状態にします。
そのアプリケーションの名前は、Count-Serverにします。
次に、新しいクラスCounterを追加します。
Counter.hの内容を次の様に変更します。
#import <Foundation/Foundation.h>
#import <Nursery/Nursery.h>
NS_ASSUME_NONNULL_BEGIN
@interface Counter : NSObject <NUCoding, NUMovingUp>
//カウント
@property (nonatomic, readonly) NUInt64 count;
//countをインクリメントする
- (void)increment;
//countをデクリメントする
- (void)decrement;
//データベースから最新の状態を取得する
- (void)moveUp;
//保存終了後に呼び出される
- (void)didSave;
@property (nonatomic, nullable, assign) NUBell *bell;
@end
NS_ASSUME_NONNULL_END
さらに、Counter.mの内容を次の様に変更します。
#import "Counter.h"
@interface Counter ()
//カウントの差分
@property (nonatomic) NUInt64 countDifference;
@end
@implementation Counter
@synthesize count;
- (NUInt64)count
{
//データベースから読み込んだ時点のcountと、
//インクリメント/デクリメント操作での変化分の和を計算して返す
return count + [self countDifference];
}
//countをインクリメントする
- (void)increment
{
//実際にはcountではなくcountDifferenceをインクリメントする
[self setCountDifference:[self countDifference] + 1];
[[self bell] markChanged];
}
//countをデクリメントする
- (void)decrement
{
//実際にはcountではなくcountDifferenceをデクリメントする
[self setCountDifference:[self countDifference] - 1];
[[self bell] markChanged];
}
+ (BOOL)automaticallyEstablishCharacter
{
return YES;
}
//データベース内でのクラス定義を行う
+ (void)defineCharacter:(NUCharacter *)aCharacter on:(NUGarden *)aGarden
{
//NUInt64(64ビット符号付き整数)型のインスタンス変数としてcountを追加する
[aCharacter addInt64IvarName:@"count"];
}
- (void)encodeWithAliaser:(NUAliaser *)anAliaser
{
[anAliaser encodeInt64:[self count] forKey:@"count"];
}
- (instancetype)initWithAliaser:(NUAliaser *)anAliaser
{
self = [super init];
//今回はmoveUpWithAliaser:を使っても問題ない
[self moveUpWithAliaser:anAliaser];
return self;
}
//データベースから最新のインスタンス変数の値を自身に反映する
- (void)moveUp
{
//自身が所属するガーデンにmoveUpObject:メッセージを送る事で、
//自身のmoveUpWithAliaser:が呼び出される
[[[self bell] garden] moveUpObject:self];
}
//データベース上の最新の状態を読み込む
- (void)moveUpWithAliaser:(NUAliaser *)anAliaser
{
//最新のcountを読み込む
count = [anAliaser decodeInt64ForKey:@"count"];
}
//保存終了後に呼び出される
- (void)didSave
{
//データベース上のカウンターオブジェクトは、複数のプロセスによって変更される可能性があるため、
//最新のcountを読み込む
[self moveUp];
//保存が済んだので、カウントの差分は0に初期化する
[self setCountDifference:0];
}
@end
Counterクラスの定義が終わったら、AppDelegate.mを次の様に変更します。
#import "AppDelegate.h"
#import <Nursery/Nursery.h>
#import "Counter.h"
@interface AppDelegate ()
@property (strong) IBOutlet NSWindow *window;
@property (nonatomic) NUMainBranchNursery *nursery;
@property (nonatomic) NUNurseryNetService *service;
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
//データベースを保存するファイルパスを取得
NSString *aFilepath = [NSHomeDirectory() stringByAppendingPathComponent:@"Count-Server"];
//ファイルパスを指定してデータベースを表すNurseryオブジェクトを作成
[self setNursery:[NUMainBranchNursery nurseryWithContentsOfFile:aFilepath]];
//データベースに保存するオブジェクトを管理するGardenオブジェクトを作成
NUGarden *aGarden = [[self nursery] makeGarden];
if (![aGarden root])
{
//Gardenのルートオブジェクトが存在しない場合
//データベースに保存するCounterオブジェクトを作成
Counter *aCounter = [Counter new];
//CounterオブジェクトをGardenのルートに設定
[aGarden setRoot:aCounter];
//メモリ上のオブジェクトをデータベースファイルに保存
[aGarden farmOut];
}
//サービス名を指定してサービスのインスタンスを作成
NUNurseryNetService *aNurseryNetService = [NUNurseryNetService netServiceWithNursery:[self nursery] serviceName:@"Count-Server"];
//サービスを開始する
[aNurseryNetService start];
}
- (void)applicationWillTerminate:(NSNotification *)aNotification
{
//サービスを停止する
[[self service] stop];
}
- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app {
return YES;
}
@end
App Sandboxの設定を変更します。
次の画面を参考に、Signing & CapabilitiesタブのApp Sandbox > NetworkのIncoming Connections (Server)にチェックを入れます。
クライアントアプリケーションを作成する
サーバーアプリケーションの作成が済んだら、次はクライアントアプリケーションを作成します。
はじめてのNursery その2 実際に使ってみる を参考にして、新しいアプリケーションプロジェクトを作成し、Nurseryフレームワークを使える状態にします。
そのアプリケーションの名前は、Count-Clientにします。
先ほど作成したサーバーアプリケーションのCounterクラスのCounter.h、Counter.mをクライアントアプリケーションのプロジェクトにコピーして追加します。
次に、AppDelegate.mの内容を次の様に変更します。
#import "AppDelegate.h"
#import "Counter.h"
@interface AppDelegate ()
@property (strong) IBOutlet NSWindow *window;
@property (strong) IBOutlet NSTextField *counterTextField;
@property (nonatomic) NUBranchNursery *nursery;
@property (nonatomic) NUGarden *garden;
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
//サーバーのサービス名を指定してNUBranchNurseryのインスタンスを作成する
[self setNursery:[NUBranchNursery branchNurseryWithServiceName:@"Count-Server"]];
//NurseryからGardenを作成する
[self setGarden:[[self nursery] makeGarden]];
//GardenからCounter(ルート)を取得する
Counter *aCounter = [[self garden] root];
//カウントのテキストフィールドを更新する
[[self counterTextField] setObjectValue:@([aCounter count])];
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
// Insert code here to tear down your application
}
- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app {
return YES;
}
- (IBAction)increment:(id)sender
{
//Gardenからカウンターを取得する
Counter *aCounter = [[self garden] root];
//カウンターをインクリメントする
[aCounter increment];
//カウントのテキストフィールドを更新する
[[self counterTextField] setObjectValue:@([aCounter count])];
}
- (IBAction)decrement:(id)sender
{
//Gardenからカウンターを取得する
Counter *aCounter = [[self garden] root];
//カウンターをデクリメントする
[aCounter decrement];
//カウントのテキストフィールドを更新する
[[self counterTextField] setObjectValue:@([aCounter count])];
}
- (IBAction)save:(id)sender
{
//Gardenからカウンターを取得する
Counter *aCounter = [[self garden] root];
//保存に成功するか失敗するまで繰り返す
while (YES)
{
//オブジェクトをデータベースに保存しその結果を得る
NUFarmOutStatus aStatus = [[self garden] farmOut];
//保存に成功した場合
if (aStatus == NUFarmOutStatusSucceeded)
{
//Counterオブジェクトに保存が終了したことを知らせる
[aCounter didSave];
break;
}
//Nurseryのgradeが一致しない以外の理由で、保存に失敗した場合
if (aStatus == NUFarmOutStatusFailed)
break;
//Nurseryのgradeが一致せず、保存に失敗した場合
if (aStatus == NUFarmOutStatusNurseryGradeUnmatched)
{
//Gardenを最新の状態にする
[[self garden] moveUp];
//カウンターを更新する
[aCounter moveUp];
}
}
//カウントのテキストフィールドを更新する
[[self counterTextField] setObjectValue:@([aCounter count])];
}
@end
App Sandboxの設定を変更します。
次の画面を参考に、Signing & CapabilitiesタブのApp Sandbox > NetworkのOutgoing Connections (Client)にチェックを入れます。
次に、画面を作成します。
このアプリケーションでは、UI周りの処理にCocoaバインディングを使わず、いわゆるコントローラーが頑張るMVCで構成します。
参考: 使わないと損をするModel-View-Controller -----Smalltalkの設計指針-----
MainMenu.xibを開き、下の画面の様に、テキストフィールドと、+ボタン、-ボタン、Saveボタンを追加します。
AppDelegate.mのcounterTextFieldをウインドウのテキストフィールドと、- (IBAction)increment:(id)senderを+ボタン、- (IBAction)decrement:(id)senderを-ボタン、- (IBAction)save:(id)senderをSaveボタンと接続します。
アプリケーションを実行する
Count-ServerをXcodeから実行します。
次のように何もないデフォルトのウインドウが表示されます。
次に、Count-ClientをXcodeから実行します。
次の様にカウンターの初期値である0がテキストフィールドに表示されます。
この時点では、まだ保存をしていないためデータベース上のCounterオブジェクトの値は0です。
次に、Counter-Clientを実行させたまま、
Counter-ClientのXcodeプロジェクトからもう一度アプリケーションを実行します。
次の画面が表示されるので、表示された画面でAddを選択して、新しいアプリケーションインスタンスを実行します。
データベース上のCounterオブジェクトのカウントはまだ0なので、画面には0が表示されます。
最初に実行した方のCount-Clientアプリケーションのウインドウを選択して、Saveボタンを押します。データベース上のCounterオブジェクトが変更され、その値が1になります。
次に、2番目に実行した方のCount-Clientアプリケーションのウインドウを選択します。そして+ボタンを押して、カウントを増やします。画面上のカウントの表示が1になります。
ここが重要なところですが、データベース上のCounterオブジェクトの値はすでに1になっています。もし2番目に実行したCount-Clientアプリケーションの保存処理を実行した結果、データベース上のCounterオブジェクトの値が1で上書きされてしまうとすると、最初の変更が無かった事になり、整合性が失われます。
最初と2番目のCount-Clientアプリケーションで、それぞれ1つずつカウントを増やしているので、正しい結果は2です。
では、実際にはどうなるかを確認します。
2番目に実行した方のCount-Clientのsaveボタンを押します。
結果は、正しく2になりました。
Nurseryデータベースの競合状態を検出する仕組み
NUNurseryのサブクラスのインスタンスはデータベースへの変更時点を識別するgradeという値を持ちます。また、NUGardenのインスタンスはそれぞれ、データベースから最初にオブジェクトを読み込んだ時点のgrade値を持ちます。gradeは単純にデータベースの変更時点を識別するために存在し、バージョン管理機能を提供するものではありません。
Nurseryデータベースにオブジェクトを保存するには、NUGardenオブジェクトのfarmOutメソッドを使いますが、データベースが変更される度にNurseryはgradeを更新します。
そして、farmOutメソッドの実行時に、NUGardenオブジェクトのgradeとNurseryのgradeが同じであるかを比較し、もし一致しなければ、保存処理を中断し、NUFarmOutStatusNurseryGradeUnmatchedをメソッドの返り値として返します。
Count-Clientアプリケーションのsave:メソッドの次の部分では、farmOutメソッドからNUFarmOutStatusNurseryGradeUnmatchedが返された場合の処理を行っています。
gradeが一致しなかった場合は、まずGardenオブジェクトをmoveUpでgrade等の情報を最新化して、次にデータベースに保存するCounterオブジェクトを最新化しています。
また、複数プロセスがデータベースを変更する可能性があるため、メモリ上のオブジェクトを最新化しても、それをデータベースに反映する前に、再び他のプロセスによってデータベースが変更される可能性があるため、保存処理をループ内で実行しています。
save:メソッドの抜粋
...省略
//保存に成功するか失敗するまで繰り返す
while (YES)
{
...省略
//Nurseryのgradeが一致せず、保存に失敗した場合
if (aStatus == NUFarmOutStatusNurseryGradeUnmatched)
{
//Gardenを最新の状態にする
[[self garden] moveUp];
//カウンターを更新する
[aCounter moveUp];
}
}
...省略
Counterクラスの最新化処理の抜粋
//データベースから最新のインスタンス変数の値を自身に反映する
- (void)moveUp
{
//自身が所属するガーデンにmoveUpObject:メッセージを送る事で、
//自身のmoveUpWithAliaser:が呼び出される
[[[self bell] garden] moveUpObject:self];
}
//データベース上の最新の状態を読み込む
- (void)moveUpWithAliaser:(NUAliaser *)anAliaser
{
//最新のcountを読み込む
count = [anAliaser decodeInt64ForKey:@"count"];
}
次回
次回はキーオブジェクトからそれに対応すオブジェクトに効率的にアクセス可能なB+木を実装したコレクションクラスNULibraryの使い方を紹介する予定です。