- 原文著者:Alberto De Bortoli
- 訳文出典:掘金翻訳計画
- 翻訳者:Zheaoli
- 校正者:Kulbear, cbangchen
過去数ヶ月間、私は Core Data の研究に多くの時間を費やしました。古いコードや悪い Core Data、マルチスレッドの安全性に違反したプロジェクトを扱う必要がありました。正直なところ、Core Data を学ぶのは非常に難しいです。Core Data を学んでいるとき、あなたは混乱し、深い挫折感を感じることでしょう。これらの理由から、私は超簡単な解決策を提供することに決めました。この解決策の特徴は、シンプルでスレッドセーフであり、非常に使いやすいことです。この解決策は、Core Data に関するあなたのほとんどのニーズを満たすことができます。いくつかの反復を経て、私が設計した解決策は最終的に成熟したものとなりました。
さて、皆さん、今からSkiathosとSkopelosを紹介させていただきます。SkiathosはObjective-Cを基に開発され、SkopelosはSwiftを基に開発されています。この 2 つのフレームワークの名前は、ギリシャの 2 つの島に由来しており、私は 2016 年の夏をここで過ごし、同時にこの 2 つのフレームワークの作成を完了しました。
前書き#
このプロジェクトの目的は、あなたが簡単に Core Data をアプリに導入できるようにすることです。
以下のいくつかの側面から紹介します:
- CoreDataStack
- AppStateReactor
- DALService (データアクセス層)
CoreDataStack#
Core Data を使用した経験があるなら、スタックを作成することが罠だらけのプロセスであることを知っているでしょう。このコンポーネントはスタックを作成するために使用されます(Object Contextを管理するため)。具体的な設計の説明は、Marcus Zarra が書いたこの記事を参照してください。
Magical Record や他のサードパーティのプラグインと異なる点の一つは、全体のストレージプロセスが一方向で開始されることです。これは、ある子ノードから下に、または上に持続的に保存されることを意味します。他のコンポーネントは、private contextを親ノードとして持つ子ノードを作成することを許可します。これにより、main contextは更新できなくなり、通知を通じてのみマージ更新が行われます。main contextは比較的固定されており、UIとバインドされています。このようなシンプルな方法は、開発者がアプリの開発をより良く行うのに役立ちます。
AppStateReactor#
うーん、実際にはこの部分は無視しても構いません。このコンポーネントは CoreDataStack に属し、アプリがバックグラウンドに切り替わったり、ノードを失ったり、終了しようとしているときに、対応する変更を監視し、それを保存する役割を果たします。
DALService (データアクセス層) / (Skiathos/Skopelos)#
Core Data を使用した経験があるなら、私たちのほとんどの操作が繰り返しであることを知っているでしょう。私たちはしばしばあるコンテキスト内でperformBlock:/performBlockAndWait:
関数を呼び出し、このコンテキストは最終的にsave:
を呼び出すブロックを提供します。データベースのすべての操作は、API で提供されるread:
とwrite:
に基づいています。この 2 つのプロトコルは CQRS(コマンドとクエリの分離)の実装を提供します。読み取り用のコードブロックはメインスレッドで実行されます(これは確定した単一のリソースと見なされます)。書き込み用のコードブロックはサブスレッドで実行され、リアルタイムでデータを保存することが保証されます。変更されたデータは、メインスレッドをブロックすることなく非同期的に保存されます。write:completion:
メソッドは、プログラムの実行後にデータの変更を持続的に保存します。
言い換えれば、書き込まれたデータはmain managed object context
と最終的な持続化プロセスの両方で一貫性が保証されます。主要な管理オブジェクトのcontext
内でも、対応するデータの可用性が保証されます。
Skiathos
/Skopelos
はDALService
のサブクラスであり、このコンポーネントに良い名前を付けることができます。
使用方法#
この一連のコンポーネントを使用する前に、まずSkiathos
型のプロパティを作成し、次のように初期化する必要があります:
self.skiathos = [Skiathos setupInMemoryStackWithDataModelFileName:@"<#datamodelfilename>"];
// または
self.skiathos = [Skiathos setupSqliteStackWithDataModelFileName:@"<#datamodelfilename>"];
Skopelos
を使用する場合、コードは次のようになります:
self.skopelos = SkopelosClient(inMemoryStack: "<#datamodelfilename>")
// または
self.skopelos = SkopelosClient(sqliteStack: "<#datamodelfilename>")
依存性注入を使用して、アプリの他の場所でこれらのオブジェクトを使用できます。Core Data スタック上の異なるオブジェクトにシングルトンを作成することは非常に良い方法だと言わざるを得ません。もちろん、インスタンスを継続的に作成するコストは非常に大きいです。一般的に、私たちはシングルトンパターンの使用をあまり推奨していません。シングルトンパターンはテスト性が低く、使用中に使用者がそのライフサイクルを効果的に制御できないため、いくつかのベストプラクティスのプログラミング原則に反する可能性があります。したがって、このライブラリではシングルトンの使用を推奨していません。
以下の理由から、Skiathos
/Skopelos
から継承する必要があります:
- グローバルに共有可能なインスタンスを作成する。
handleError(error: NSError)
メソッドをオーバーロードし、プログラム内でエラーが発生したときにこのメソッドが正常に呼び出されるようにする。
シングルトンを作成するには、以下の例のようにSkiathos
/Skopelos
から継承する必要があります:
シングルトン#
@interface SkiathosClient : Skiathos
+ (SkiathosClient *)sharedInstance;
@end
static SkiathosClient *sharedInstance = nil;
@implementation SkiathosClient
+ (SkiathosClient *)sharedInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [self setupSqliteStackWithDataModelFileName:@"<#datamodelfilename>"];
</#datamodelfilename> });
return sharedInstance;
}
- (void)handleError:(NSError *)error
{
// クライアントはここで適切な処理を行うべきです
NSLog(@"%@", error.description);
}
@end
または
class SkopelosClient: Skopelos {
static let sharedInstance = Skopelos(sqliteStack: "DataModel")
override func handleError(error: NSError) {
// クライアントはここで適切な処理を行うべきです
print(error.description)
}
}
読み書き操作#
ここまで書いたので、標準的な Core Data の操作方法と私たちのコンポーネントが提供する方法を見てみましょう。
標準の読み取り方法:
__block NSArray *results = nil;
NSManagedObjectContext *context = ...;
[context performBlockAndWait:^{
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:NSStringFromClass(User)
inManagedObjectContext:context];
[request setEntity:entityDescription];
NSError *error;
results = [context executeFetchRequest:request error:&error];
}];
return results;
標準の書き込み方法:
NSManagedObjectContext *context = ...;
[context performBlockAndWait:^{
User *user = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass(User)
inManagedObjectContext:context];
user.firstname = @"John";
user.lastname = @"Doe";
NSError *error;
[context save:&error];
if (!error)
{
// ストアに保存を続ける
}
}];
Skiathos
での読み取り方法:
[[SkiathosClient sharedInstance] read:^(NSManagedObjectContext *context) {
NSArray *allUsers = [User allInContext:context];
NSLog(@"すべてのユーザー: %@", allUsers);
}];
Skiathos
での書き込み方法:
// 同期
[[SkiathosClient sharedInstance] writeSync:^(NSManagedObjectContext *context) {
User *user = [User createInContext:context];
user.firstname = @"John";
user.lastname = @"Doe";
}];
[[SkiathosClient sharedInstance] writeSync:^(NSManagedObjectContext *context) {
User *user = [User createInContext:context];
user.firstname = @"John";
user.lastname = @"Doe";
} completion:^(NSError *error) {
// 変更が永続ストアに保存されます
}];
// 非同期
[[SkiathosClient sharedInstance] writeAsync:^(NSManagedObjectContext *context) {
User *user = [User createInContext:context];
user.firstname = @"John";
user.lastname = @"Doe";
}];
[[SkiathosClient sharedInstance] writeAsync:^(NSManagedObjectContext *context) {
User *user = [User createInContext:context];
user.firstname = @"John";
user.lastname = @"Doe";
} completion:^(NSError *error) {
// 変更が永続ストアに保存されます
}];
Skiathos
はもちろん、チェーン呼び出しもサポートしています:
__block User *user = nil;
[SkiathosClient sharedInstance].write(^(NSManagedObjectContext *context) {
user = [User createInContext:context];
user.firstname = @"John";
user.lastname = @"Doe";
}).write(^(NSManagedObjectContext *context) {
User *userInContext = [user inContext:context];
[userInContext deleteInContext:context];
}).read(^(NSManagedObjectContext *context) {
NSArray *users = [User allInContext:context];
});
Swift の場合、コードは次のようになります:
読み取り:
SkopelosClient.sharedInstance.read { context in
let users = User.SK_all(context)
print(users)
}
書き込み:
// 同期
SkopelosClient.sharedInstance.writeSync { context in
let user = User.SK_create(context)
user.firstname = "John"
user.lastname = "Doe"
}
SkopelosClient.sharedInstance.writeSync({ context in
let user = User.SK_create(context)
user.firstname = "John"
user.lastname = "Doe"
}, completion: { (error: NSError?) in
// 変更が永続ストアに保存されます
})
// 非同期
SkopelosClient.sharedInstance.writeAsync { context in
let user = User.SK_create(context)
user.firstname = "John"
user.lastname = "Doe"
}
SkopelosClient.sharedInstance.writeAsync({ context in
let user = User.SK_create(context)
user.firstname = "John"
user.lastname = "Doe"
}, completion: { (error: NSError?) in
// 変更が永続ストアに保存されます
})
チェーン呼び出し:
SkopelosClient.sharedInstance.write { context in
user = User.SK_create(context)
user.firstname = "John"
user.lastname = "Doe"
}.write { context in
if let userInContext = user.SK_inContext(context) {
userInContext.SK_remove(context)
}
}.read { context in
let users = User.SK_all(context)
print(users)
}
NSManagedObject
クラスは非常に明確なCRUDメソッドを提供しています。読み取り / 書き込みコードブロックのパラメータとして渡す際、オブジェクトは全体として処理されるべきです。これらの組み込みメソッドを優先的に使用するべきです。主なメソッドは以下の通りです:
+ (instancetype)SK_createInContext:(NSManagedObjectContext *)context;
+ (NSUInteger)SK_numberOfEntitiesInContext:(NSManagedObjectContext *)context;
- (void)SK_deleteInContext:(NSManagedObjectContext *)context;
+ (void)SK_deleteAllInContext:(NSManagedObjectContext *)context;
+ (NSArray *)SK_allInContext:(NSManagedObjectContext *)context;
+ (NSArray *)SK_allWithPredicate:(NSPredicate *)pred inContext:(NSManagedObjectContext *)context;
+ (instancetype)SK_firstInContext:(NSManagedObjectContext *)context;
static func SK_create(context: NSManagedObjectContext) -> Self
static func SK_numberOfEntities(context: NSManagedObjectContext) -> Int
func SK_remove(context: NSManagedObjectContext) -> Void
static func SK_removeAll(context: NSManagedObjectContext) -> Void
static func SK_all(context: NSManagedObjectContext) -> [Self]
static func SK_all(predicate: NSPredicate, context:NSManagedObjectContext) -> [Self]
static func SK_first(context: NSManagedObjectContext) -> Self?
注意:SK_inContext(context: NSManagedObjectContext)
を使用する際、異なる読み書きコードブロックが同じオブジェクトを取得する可能性があります。
スレッドセーフ#
すべての DALService によって生成されたインスタンスはスレッドセーフであると見なすことができます。
私たちは特にプロジェクトで-com.apple.CoreData.ConcurrencyDebug 1
の設定を行うことをお勧めします。これにより、マルチスレッドおよび並行処理の状況で Core Data を乱用しないことが保証されます。
このコンポーネントは、ManagedObjectContext:
の概念を隠すことによってインターフェースを導入することを目的としたものではありません。これは、クライアント側でより多くのスレッドの問題を引き起こすことになります。なぜなら、開発者が呼び出されたスレッドのタイプを確認する責任を負うことになるからです(それは Core Data がもたらす利点を無視することになります)。