- 原文作者:Alberto De Bortoli
- 译文出自:掘金翻譯計劃
- 译者:Zheaoli
- 校對者:Kulbear, cbangchen
在過去的幾個月裡,我花費了大量的時間在研究 Core Data 之上,我得去處理一個使用了很多陳舊的代碼,糟糕的 Core Data 以及違反了多線程安全的項目。講真,Core Data 學習起來非常的困難,在學習 Core Data 的時候,你肯定會感到困惑和一種深深的挫敗感。正是因為這些原因,我決定給出一種超級簡單的解決方案。這個方案的特點就是簡潔,線程安全,非常易於使用,這個方案能滿足你大部分對於 Core Data 的需求。在經過若干次的迭代後,我所設計的方案最終成為一個成熟的方案。
OK,女士們,先生們,現在請允許我隆重向您介紹 Skiathos 和 Skopelos。其中 Skiathos 是基於 Objective-C 所開發的,而 Skopelos 則基於 Swift 所開發的。這兩個框架的名字來源於希臘的兩個島,在這裡,我度過了 2016 年的夏天,同時,在這裡完成了兩個框架的編寫工作。
寫在前面的话#
整個項目的目的就是能夠讓您以及其簡便的方式在您的 App 中引入 Core Data。
我們將從如下幾個方面來進行一個介紹:
- CoreDataStack
- AppStateReactor
- DALService (Data Access Layer)
CoreDataStack#
如果你有過使用 Core Data 的經驗,那麼你應該知道創建一個堆棧是一個充滿陷阱的過程。這個組件是用於創建堆棧(用於管理 Obejct Context ),具體的設計說明可以參看 Marcus Zarra 所寫的這篇文章。
其中一個和 Magical Record 或者其餘第三方插件不同的是,整個存儲過程都是在一個方向上發起的,可能是從某個子節點向下或者向上傳遞來進行持久化儲存。其餘的組件允許你創建以 private context 作為父節點的子節點,這將會導致 main context 不能被更新,同時只能通過通知的方式來進行合併更新。main context 是相對固定的並與 UI 進行了綁定:這樣較為簡單的方式可以幫助開發者更好的去完成一個 APP 的開發。
AppStateReactor#
唔,其實你可以忽略這一段。這個組件屬於 CoreDataStack ,在 App 切換至後台,失去節點,或者即將退出時,它負責監視相對應的修改,並把其保存。
DALService (Data Access Layer) / (Skiathos/Skopelos)#
如果你擁有使用 Core Data 的經驗,那麼你也應該知道,我們大部分操作都是重複的,我們經常在一個 context 中調用 performBlock:/performBlockAndWait:
函數,而這個 Context 提供了一個最終會調用 save:
作為最終語句的 block 。數據庫的所有操作都是基於 API 中所提供的 read:
和 write:
:這兩個協議提供了 CQRS (命令和查詢分離) 的實現。用於讀取的代碼塊將在主體中進行運行(因為這被認為是一個已確定的單個資源)。用於寫入的代碼塊將會在一個子線程中運行,這樣可以保證實時的進行數據儲存,變化的數據將會在不會阻塞主線程的情況下通過異步的方式進行儲存。write:completion:
方法將會程序運行完來對數據的更改進行持久化儲存。
換句話說,寫入的數據在 main managed object context
和最後持久化過程中都會保證其一致性。在 主要管理對象的 context
中,相應的數據也能保證其可用性。
Skiathos
/Skopelos
是 DALService
的子類,這樣可以給這個組件一個比較好聽的名字。
使用介紹#
在使用這一系列組件之前,你首先需要創建一個類型為 Skiathos
的屬性,然後以下面這種方式去初始化它:
self.skiathos = [Skiathos setupInMemoryStackWithDataModelFileName:@"<#datamodelfilename>"];
// or
self.skiathos = [Skiathos setupSqliteStackWithDataModelFileName:@"<#datamodelfilename>"];
在使用 Skopelos
時,代碼如下所示:
self.skopelos = SkopelosClient(inMemoryStack: "<#datamodelfilename>")
// or
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
{
// clients should do the right thing here
NSLog(@"%@", error.description);
}
@end
或者是
class SkopelosClient: Skopelos {
static let sharedInstance = Skopelos(sqliteStack: "DataModel")
override func handleError(error: NSError) {
// clients should do the right thing here
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)
{
// continue to save back to the store
}
}];
Skiathos
中的讀取姿勢:
[[SkiathosClient sharedInstance] read:^(NSManagedObjectContext *context) {
NSArray *allUsers = [User allInContext:context];
NSLog(@"All users: %@", allUsers);
}];
Skiathos
中的寫入姿勢:
// Sync
[[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) {
// changes are saved to the persistent store
}];
// Async
[[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) {
// changes are saved to the persistent store
}];
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)
}
寫入:
// Sync
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
// changes are saved to the persistent store
})
// Async
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
// changes are saved to the persistent store
})
鏈式調用:
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: NSManagerObjectContext)
時,不同的讀寫代碼塊可能會得到同一個對象。
線程安全#
所有 DALService 所產生的實例都可以認為是線程安全的。
我們特別建議你在項目中進行這樣的設置 -com.apple.CoreData.ConcurrencyDebug 1
,這可以確保你不會在多線程和並發的情況下濫用 Core Data。
這個組件不是為了通過隱藏 ManagedObjectContext:
的概念來達到接口引入的目的:它將會在客戶端中引入更多的線程問題,因為開發者有責任去檢查所調用線程的類型(而那將會是在忽視 Core Data 所帶給我們的好處)。