JustPersist
JustPersist是使用Core Data开箱即用支持来做iOS持久化的最简单、最安全的方法。它还允许您以最小的努力迁移到任何其他的持久化框架。
概述
在Just Eat,我们在iOS应用程序中持久化各种数据。2014年,我们决定使用MagicalRecord作为Core Data的包装器,但随着时间的推移,出现了许多问题和基本的线程安全问题。2017年,MagicalRecord不再受支持,新的解决方案看起来更有吸引力。我们决定采用Skopelos:一个更年轻、更轻量级的Core Data框架,设计更简单,由我们的一位工程师Alberto De Bortoli开发。持久化层接口的设计也得到了Skopelos的启发,我们邀请读者查看其文档。
采用新的持久化解决方案的主要问题是迁移到它。这通常并不容易,尤其是如果遗留代码库没有隐藏所采用的框架(在我们的案例中是MagicalRecord),而是在视图控制器、管理器、助手类、类别和有时视图中到处传播。最终,在Core Data的情况下,只有一个持久存储,这足以使不可能一次移动访问。一次只能有一个活跃的持久化解决方案。
我们认为这是一个非常常见的问题,尤其是在移动领域。我们创建JustPersist正是出于这个原因,以及简化迁移过程。
最终,JustPersist有两点
- 一个持久化层,具有清晰简单的接口,用于执行事务性的读取和写入(Skopelos风格的)
- 一个解决方案,可以从一个持久化层迁移到另一个持久化层,我们认为这是可能的最小努力
JustPersist旨在成为iOS上最简单、最安全的数据持久化方式。它支持Core Data内置,并可以扩展以透明地支持其他框架。自从从MagicalRecord迁移到Skopelos以来,我们提供了这两个框架的可用封装器。
JustPersist的基调非常偏向Core Data,但它能够让你在实现自定义数据存储(封装器)的情况下迁移到任何其他持久化框架(如果你足够勇敢,甚至是Realm)。
JustPersist可通过CocoaPods获取。要安装它,只需将以下行添加到你的Podfile中
pod "JustPersist/Skopelos"
# or
pod "JustPersist/MagicalRecord"
仅使用pod JustPersist
将添加核心pods但没有子规范,你将不得不自己实现封装器来使用它。如果你打算将JustPersist扩展以支持其他框架,我们建议创建一个子规范。
持久化层的用法
要执行操作,你需要一个数据存储,你可以像这样设置(或参见相关段落)
let dataStore = SkopelosDataStore(sqliteStack: <modelURL>)
// or
let dataStore = MagicalRecordDataStore()
在第一次使用数据存储之前,你必须在上面调用setup()
,并且在完全完成它后,可能需要调用tearDown()
。
我们建议在应用启动时设置堆栈,在AppDelegate中的applicationDidFinishLaunchingWithOptions
方法中,并在你的整个应用的生存周期结束,在重置应用状态(如果你提供这样做的方式)或在你的单元测试套件的tearDown
方法中将其拆解。
为了隐藏底层使用的持久化框架,JustPersist提供符合DataStoreItem
和MutableDataStoreItem
协议的东西,而不是针对CoreData特定的NSManagedObject
。这些协议通过使用objectForKey
和setObject:forKey:
方法提供对属性的访问。
在Core Data的情况下,JustPersist为NSManagedObject
提供一个扩展,使其符合MutableDataStoreItem
。
读取和写入
读取和写入之间的分离是JustPersist的基础。读取在默认情况下总是同步的
dataStore.read { (accessor) in
...
}
而写入可以是同步或异步的
dataStore.writeSync { (accessor) in
...
}
dataStore.writeAsync { (accessor) in
...
}
由快照提供的访问器可以是读取的(DataStoreReadAccessor
)或读写的(DataStoreReadWriteAccessor
)。读取访问器允许你执行如下的读取操作
func items(forRequest request: DataStoreRequest) -> [DataStoreItem]
func firstItem(forRequest request: DataStoreRequest) -> DataStoreItem?
func countItems(forRequest request: DataStoreRequest) -> Int
而读写访问器允许你执行完整的一套CRUD操作
func mutableItems(forRequest request: DataStoreRequest) -> [MutableDataStoreItem]
func firstMutableItem(forRequest request: DataStoreRequest) -> MutableDataStoreItem?
func createItem(ofMutableType itemType: MutableDataStoreItem.Type) -> MutableDataStoreItem?
func insert(_ item: MutableDataStoreItem) -> Bool
func delete(item: MutableDataStoreItem) -> Bool
func deleteAllItems(ofMutableType itemType: MutableDataStoreItem.Type) -> Bool
func mutableVersion(ofItem item: DataStoreItem) -> MutableDataStoreItem?
要执行一个操作,你可能需要一个DataStoreRequest
,它可以针对itemType、NSPredicate、一个NSSortDescriptor数组、偏移量和限制进行定制。将其视为相应的Core Data的NSFetchRequest
。
以下是一些完整的示例
dataStore.read { (accessor) in
let request = DataStoreRequest(itemType: Restaurant.self)
let count = accessor.countItems(forRequest: request)
}
dataStore.read { (accessor) in
let request = DataStoreRequest(itemType: Restaurant.self)
request.setFilter(whereAttribute: "name", equalsValue: <some_name>)
guard let restaurant = accessor.firstItem(forRequest: request) as? Restaurant else { return }
...
}
dataStore.writeSync { (accessor) in
let restaurant = accessor.createItem(ofMutableType: Restaurant.self) as! Restaurant
restaurant.name = <some_name>
...
let wasDeleted = accessor.delete(item: restaurant)
}
在写入块中,不需要调用任何保存方法。因为它在事务块的末尾显然是应该做的,JustPersist 会为您完成。读取块不是为了修改存储,并且您甚至没有可用的 API 来进行这样做(除非在 CoreData 的情况下将 DataStoreItem
对象转换为 NSManagedObject
以允许设置属性),因此底层不会执行任何保存操作。
设置数据存储的常见方式
我们建议使用依赖注入来传递数据存储,但有时可能很难。如果您希望通过单例访问数据存储,以下是使用 Skopelos 创建共享实例 DataStoreClient(例如,DataStoreClient.swift
)的示例。
@objc
class DataStoreClient: NSObject {
static let shared: DataStore = {
return DataStoreClient.sqliteStack()
}()
static let inMemoryShared: DataStore = {
return DataStoreClient.inMemoryStack()
}()
class func sqliteStack() -> DataStore {
let modelURL = Bundle.main.url(forResource: "<schema_filename>", withExtension: "momd")! // want to crash if schema is missing
return SkopelosDataStore(sqliteStack: modelURL, securityApplicationGroupIdentifier: <security_application_group_identifier_id_any>) { error in
print("Core Data error reported via SkopelosDataStore (sqliteStack): \(error.localizedDescription)")
}
}
class func inMemoryStack() -> DataStore {
let modelURL = Bundle.main.url(forResource: "<schema_filename>", withExtension: "momd")! // want to crash if schema is missing
return SkopelosDataStore(inMemoryStack: modelURL) { error in
print("Core Data error reported via SkopelosDataStore (inMemoryStack): \(error.localizedDescription)"")
}
}
对于单元测试,您可能会想使用 inMemoryShared
以获得更好的性能。
子数据存储
子数据存储在需要回滚应用程序特定部分或用户旅程中执行的所有变更的情况下非常有用。把它看作是 Marcus Zarra 的 Core Data 栈 中的 scratch/disposable 上下文。
在 Just Eat,我们使用子数据存储来向购物车添加复杂产品。用户可能对产品进行多次更新,当用户确认添加时执行最终保存操作比在主数据存储上处理多个 CRUD 操作更简单。
子数据存储的行为就像一个普通的数据存储一样,唯一的例外是,要保存更改回主数据存储,开发人员必须显式合并数据存储。以下是一个完整的示例
let childDataStore = dataStore.makeChildDataStore()
childDataStore.setup()
...
dataStore.merge(childDataStore)
childDataStore.tearDown()
线程安全注意事项
读取和同步写入块始终在主线程上执行,无论哪个线程调用它们。异步写入块始终在后台线程上执行。
同步写入仅在更改持久后返回(在 Core Data 的情况下,通常是到具有主并发类型的 NSManagedObjectContext
)。
异步写入操作会立即返回,并将保存到真相源的任务留给 JustPersist(无论是上下文还是持久化存储)。它们是最终一致的,这意味着下一次读取可能无法立即获得数据。
强制使用事务性编程模型进行读取和写入,有助于开发者避免线程安全问题,在 Core Data 中可以通过设置方案中的 -com.apple.CoreData.ConcurrencyDebug 1
标志来捕捉这些问题(我们建议启用该标志)。
如何迁移到不同的持久化层
本节中的示例代码使用 Objective-C 编写,原因有二:1. 它们处理了示例的遗留代码;2. 为了展示 JustPersist 也能很好地与 Objective-C 一起使用。
在此,我们将概述我们从 MagicalRecord 迁移到 Skopelos 使用 JustPersist 所采取的步骤。我们相信很多应用仍然使用 MagicalRecord,所以这可能也适用于您的案例。如果您需要从和到其他 2 个框架迁移,您需要实现相应的数据存储器以包装它们。
您应该从实现您的 DataStoreClient
开始(您可以参考相关段落中的步骤),并在 sqliteStack
方法中分配您应用程序当前使用的持久化层的数据存储器,也许在 inMemoryStack
方法中也是如此。在我们的案例中,因为我们想从 MagicalRecord 迁移,所以使用的数据存储器将是 MagicalRecordDataStore
。
MagicalRecord 的标准 CRUD 交互是这样的
NSManagedObjectContext *mainContext = [NSManagedObjectContext MR_defaultContext];
NSManagedObjectContext *childContext = [NSManagedObjectContext MR_contextWithParent:mainContext];
// writing (Create)
[childContext performBlockAndWait:^{
Restaurant *restaurant = [Restaurant MR_createEntityInContext:localContext];
[childContext MR_saveToPersistentStoreAndWait];
}];
// reading (Read)
[childContext performBlockAndWait:^{
Restaurant *restaurant = [Restaurant MR_findFirstInContext:childContext];
}];
// writing (Update)
[childContext performBlockAndWait:^{
Restaurant *restaurant = [Restaurant MR_findFirstInContext:childContext];
restaurant.name = <some_name>
[childContext MR_saveToPersistentStoreAndWait];
}];
// writing (Delete)
[childContext performBlockAndWait:^{
[Restaurant MR_truncateAllInContext:localContext];
[childContext MR_saveToPersistentStoreAndWait];
}];
它们都应该逐个转换为 JustPersist
DataStore *dataStore = [DataStoreClient shared];
// writing (Create)
[dataStore writeSync:^(id<JEDataStoreReadWriteAccessor> accessor) {
Restaurant *restaurant = (Restaurant *)[accessor createItemOfMutableType:Restaurant.class];
}];
// reading (Read)
[dataStore read:^(id<JEDataStoreReadAccessor> accessor) {
JEDataStoreRequest *request = [[JEDataStoreRequest alloc] initWithItemType:Restaurant.class];
Restaurant *restaurant = (Restaurant *)[accessor firstItemForRequest:request];
}];
// writing (Update)
[dataStore writeSync:^(id<JEDataStoreReadWriteAccessor> accessor) {
JEDataStoreRequest *request = [[JEDataStoreRequest alloc] initWithItemType:Restaurant.class];
Restaurant *restaurant = (Restaurant *)[accessor firstItemForRequest:request];
restaurant.name = <some_name>
}];
// writing (Delete)
[dataStore writeSync:^(id<JEDataStoreReadWriteAccessor> accessor) {
[accessor deleteAllItemsOfMutableType:Restaurant.class];
}];
您应该确保在块中不执行任何 UI 操作,即使在 read
和 writeSync
在主线程上执行的情况下也是如此。实际上,您应该只做与与持久化层交互有关的必要工作,这通常可能意味着从对象中复制值以使它们在块外可用(在 Objective-C 中通过 __block
关键字)。开发者不应持有模型对象的引用,以在线程之间传递它们(事务性块有助于确保这项规则)。
通过将所有直接与 MagicalRecord 的交互从 MagicalRecord 迁移到 JustPersist,您现在应该能够从整个代码库中删除所有各种 @import MagicalRecord
和 #import <MagicalRecord/MagicalRecord.h>
。
在此阶段,您可以修改您的 DataStoreClient
,在 sqliteStack
和 inMemoryStack
方法中分配目标数据存储器。在我们的案例中,是 SkopelosDataStore
。
结论
JustPersist 旨在成为 iOS 上最简单和最安全的数据持久化方式。它支持 Core Data 并可以扩展以透明地支持其他框架。
您可以使用 JustPersist 以最少的努力从一层持久化层迁移到另一层。因为我们从 MagicalRecord 迁移到 Skopelos,所以我们提供了这两个框架的可用的包装器。
JustPersist的核心是一个具有清晰、简单接口的持久化层,用于执行事务性读取和写入,并从Skopelos项目汲取灵感,其中读取和写入是通过设计分隔的。
我们希望这个库能简化持久化堆栈的设置过程,避免Core Data的常见难题和潜在的线程问题。
- Just Eat iOS团队