Skopelos
这是一个针对 Core Data 的精简、线程安全的轻量级、无样板代码、极易使用的 Active Record 版本。简单直观,满足您对 Core Data 的所有需求。Swift 4 版本。
一般说明
本组件旨在以几乎零开销将 Core Data 引入您的应用程序,具有极其简单的接口。
这里引入的设计涉及几个主要组件
- CoreDataStack
- AppStateReactor
- DALService (数据访问层)
CoreDataStack
如果您有 Core Data 的经验,您可能知道创建堆栈是一个令人苦恼的过程,充满了陷阱。本组件负责使用以下设计创建堆栈(以管理对象上下文链的形式):由 Marcus Zarra 在此处介绍。
Managed Object Model <------ Persistent Store Coordinator ------> Persistent Store
^
|
Root Context (NSPrivateQueueConcurrencyType)
^
|
------------> Main Context (NSMainQueueConcurrencyType) <-------------
| ^ |
| | |
Scratch Context Scratch Context Scratch Context
(NSPrivateQueueConcurrencyType) (NSPrivateQueueConcurrencyType) (NSPrivateQueueConcurrencyType)
与 Magical Record 或其他第三方库相比,一个重要的区别是存储总是单向的,从零环境(如图所示的方向向上)到持久存储。其他组件允许您创建具有私有环境作为父级的临时环境,这会导致主环境不更新或通过通知合并环境进行更新。主环境应该是真相之源,并且与 UI 相关联:采用更简单的方法有助于创建更易于推理的系统。
AppStateReactor
请忽略此部分。它位于 CoreDataStack 中,负责在应用进入后台、失去焦点或即将终止时将暂时更改保存回磁盘。它是一个默默无闻的朋友,在必要时照顾我们。
DALService (数据访问层) / Skopelos
如果你有 Core Data 的经验,你可能也知道,大多数操作都是重复的,我们通常会调用 performBlock
/performBlockAndWait
在一个上下文中提供一个块,该块最终会调用 save:
作为最后一个语句。数据库都是关于读取和写入的,因此我们的 API 以 read(statements: NSManagedObjectContext -> Void)
和 writeSync(changes: NSManagedObjectContext -> Void)
/writeAsync(changes: NSManagedObjectContext -> Void)
的形式提供:2 个协议,提供 CQRS (命令和查询责任分离) 方法。读取块将在主上下文中执行(因为它被认为是单一生成源)。写入块将在一个临时上下文中执行,并最后保存;更改最终将异步保存回持久存储,不会阻塞主线程。写入方法的完成处理程序在更改保存到持久存储后调用完成处理程序。
换句话说,写入在现代管理对象上下文中总是一致的,在持久存储中最终是一致的。数据总是在现代管理对象上下文中可用。
Skopelos
只是 DALService
的一个子类,为该组件提供了一个好听的名字。
如何使用
导入 Skopelos
。
要使用此组件,您可以创建一个类型为 Skopelos
的属性,并按如下方式实例化它
self.skopelos = Skopelos(sqliteStack: "<#ModelURL>")
或
self.skopelos = Skopelos(sqliteStack: "<#ModelURL>", securityApplicationGroupIdentifier: "<#GroupID>")
或
self.skopelos = Skopelos(inMemoryStack: "<#ModelURL>")
注意。上述所有方法也可以接受一个额外的可选参数 allowsConcurrentWritings
(默认为false),允许每个写入操作使用专用的擦除上下文。对于简单的应用程序,写入时重复使用相同的擦除上下文(即使用默认值)有助于避免更改推送到主上下文时的竞态条件。
虽然将 Skopelos
视为单例是可以接受的,但始终最好不使用此类模式,而是显式创建一个实例并将其通过依赖注入注入到应用的各个部分中。一般来说,我们不使用单例。它们本质上是不可测试的,客户端无法控制对象的生命周期,并违反一些原则。因此,该库不包含单例。
您可以继承自 Skopelos
并
- 将其包裹成一个针对您用例的特定接口
- 重写
handleError(_error: NSError)
以在遇到错误时执行特定操作
以下是一个示例
protocol SkopelosClientDelegate: class {
func handle(_ error: NSError)
}
class SkopelosClient: Skopelos {
static let modelURL = Bundle(identifier: "<#com.mydomain.myapp>").url(forResource: "<#DataModel>", withExtension: "momd")!
weak var delegate: SkopelosClientDelegate?
class func sqliteStack() -> Skopelos {
return Skopelos(sqliteStack: modelURL)
}
class func inMemoryStack() -> Skopelos {
return Skopelos(inMemoryStack: modelURL)
}
override func handleError(_ error: NSError) {
DispatchQueue.main.async {
self.delegate?.handle(error)
}
}
}
读取和写入
谈到读取和写入,让我们现在比较一些标准 Core Data 代码与使用 Skopelos 编写的代码。
标准 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;
标准 Core Data 写入
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
}
}];
Skopelos 读取
skopelosClient.read { context in
let users = User.SK_all(context)
print(users)
}
Skopelos 写入
// Sync
skopelosClient.writeSync { context in
let user = User.SK_create(context)
user.firstname = "John"
user.lastname = "Doe"
}
skopelosClient.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.writeAsync { context in
let user = User.SK_create(context)
user.firstname = "John"
user.lastname = "Doe"
}
skopelosClient.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
})
Skopelos 还支持链式调用
skopelosClient.writeSync { context in
user = User.SK_create(context)
user.firstname = "John"
user.lastname = "Doe"
}.writeSync { 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 方法始终明确上下文。作为参数传递的上下文应为接收到的读取或写入块中的上下文。您应该始终在读取或写入块内使用这些方法。主要方法有
static func SK_create(context: NSManagedObjectContext) -> Self
static func SK_numberOfEntities(context: NSManagedObjectContext) -> Int
func SK_remove(context: NSManagedObjectContext)
static func SK_removeAll(context: NSManagedObjectContext)
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:
在不同的读取/写入块中检索对象(相同的读取块是安全的)。
线程安全注意事项
通过 DALService
实例完成的对持久层的所有访问都保证是线程安全的。
强烈建议在项目中启用标志 -com.apple.CoreData.ConcurrencyDebug 1
以确保您不会在线程和并发(从不同线程访问托管对象等)方面滥用 Core Data。
此组件的目的不是引入接口以隐藏 ManagedObjectContext
的概念:这将让客户端代码打开出现线程问题的大门,因为开发者应该在一定程度上负责检查调用线程的类型(这将忽视 Core Data 给我们的好处)。因此,我们的设计强制要求通过 DALService
和 ManagedObject
类别方法在所有读取和写入中都始终明确上下文(例如 SK_create
)。
客户
Skopelos被用于以下产品的生产环境中