如何实施 CoreData 自定义合并策略?
我的应用程序使用 CoreData + CloudKit 同步。一些 CoreData 实体(例如 Item
)可以通过 iCloud 的共享数据库进行共享。该应用仅使用 1 个 NSPersistentContainer
,但它有 2 个 NSManagedContexts
、visualContext
和一个 backgroundContext
。
因此,在保存上下文期间,可能会出现两种类型的合并冲突:1) 如果两个上下文都尝试在不同状态下保存相同的 Item
,以及 2) 如果我的持久容器和 iCloud 同步尝试保存不同状态下的相同Item
。
Item
有一个属性 updatedAt
,应用程序要求始终保存最后更新的 Item
版本。
出于一致性原因,我无法按属性合并。只能存储完整的 Item
对象,两者之一都存储在托管上下文中,或者存储在托管上下文中或持久存储。
但不能使用标准合并策略:NSRollbackMergePolicy
忽略托管上下文中的更改,并获取持久副本,而 NSOverwriteMergePolicy
使用托管上下文中的对象覆盖持久存储。但我必须将 Item
与最新的 updatedAt
一起使用。因此我必须使用自定义合并策略。
找到如何做到这一点的任何提示并不容易。我找到了两个带有演示代码的教程。最好的一本是 Florian Kugler 和 Daniel Eggert 所著的《Core Data》一书,其中有一节介绍自定义合并策略和相关代码 此处。另一篇是 Deepika Ramesh 的帖子,其中包含 代码。但我必须承认,我并没有完全理解两者。但根据他们的代码,我尝试设置自己的自定义合并策略,该策略将分配给两个托管上下文的 mergePolicy
属性。就是这样:
import CoreData
protocol UpdateTimestampable {
var updatedAt: Date? { get set }
}
class NewestItemMergePolicy: NSMergePolicy {
init() {
super.init(merge: .overwriteMergePolicyType)
}
override open func resolve(optimisticLockingConflicts list: [NSMergeConflict]) throws {
let nonItemConflicts = list.filter({ $0.sourceObject.entity.name != Item.entityName })
try super.resolve(optimisticLockingConflicts: nonItemConflicts)
let itemConflicts = list.filter({ $0.sourceObject.entity.name == Item.entityName })
itemConflicts.forEach { conflict in
guard let sourceObject = conflict.sourceObject as? UpdateTimestampable else { fatalError("must be UpdateTimestampable") }
let key = "updatedAt"
let sourceObjectDate = sourceObject.updatedAt ?? .distantPast
let objectDate = conflict.objectSnapshot?[key] as? Date ?? .distantPast
let cachedDate = conflict.cachedSnapshot?[key] as? Date ?? .distantPast
let persistedDate = conflict.persistedSnapshot?[key] as? Date ?? .distantPast
let latestUpdateAt = [sourceObjectDate, objectDate, cachedDate, persistedDate].max()
let persistedDateIsLatest = persistedDate == latestUpdateAt
let sourceObj = conflict.sourceObject
if let context = sourceObj.managedObjectContext {
context.performAndWait {
context.refresh(sourceObj, mergeChanges: !persistedDateIsLatest)
}
}
}
try super.resolve(optimisticLockingConflicts: itemConflicts)
}
}
我的第一个问题是这段代码是否有意义。我问这个问题是因为合并冲突很难测试。
具体来说,我显然必须在 super.init(merge: .overwriteMergePolicyType) 中使用任何标准合并属性,尽管显然哪一个并不重要,因为我正在使用自定义合并冲突解决方案。
My app uses CoreData + CloudKit synchronization. Some CoreData entities like Item
can be shared via iCloud's shared database. The app uses only 1 NSPersistentContainer
, but it has 2 NSManagedContexts
, the visualContext
and a backgroundContext
.
Thus during saving of a context, 2 types of merging conflicts can arise: 1) If both contexts try to save the same Item
in different states, and 2) If my persistent container and iCloud sync try to save the same Item
in different states.
Item
has an attribute updatedAt
, and the app requires that always the Item
version updated last should be saved.
For consistency reasons, I cannot merge by property. Only complete Item
objects can be stored, either one of both stored in a managed context, or either the one stored in a managed context or the one persistently stored.
But the standard merge policies cannot be used: NSRollbackMergePolicy
ignores changes in a managed context, and takes the persistent copy, while NSOverwriteMergePolicy
overwrites the persistent store with the object in the managed context. But I have to use the Item
with the newest updatedAt
. Thus I have to use a custom merge policy.
It was not easy to find any hint how to do this. I found two tutorials with demo code. The best one is the book Core Data by Florian Kugler and Daniel Eggert that has a section about Custom Merge Policies, and related code here. The other is a post by Deepika Ramesh with code. However I have to admit, I did not understand both fully. But based on their code, I tried to setup my own custom merge policy, that will be assigned to the mergePolicy
property of both managed contexts. Here it is:
import CoreData
protocol UpdateTimestampable {
var updatedAt: Date? { get set }
}
class NewestItemMergePolicy: NSMergePolicy {
init() {
super.init(merge: .overwriteMergePolicyType)
}
override open func resolve(optimisticLockingConflicts list: [NSMergeConflict]) throws {
let nonItemConflicts = list.filter({ $0.sourceObject.entity.name != Item.entityName })
try super.resolve(optimisticLockingConflicts: nonItemConflicts)
let itemConflicts = list.filter({ $0.sourceObject.entity.name == Item.entityName })
itemConflicts.forEach { conflict in
guard let sourceObject = conflict.sourceObject as? UpdateTimestampable else { fatalError("must be UpdateTimestampable") }
let key = "updatedAt"
let sourceObjectDate = sourceObject.updatedAt ?? .distantPast
let objectDate = conflict.objectSnapshot?[key] as? Date ?? .distantPast
let cachedDate = conflict.cachedSnapshot?[key] as? Date ?? .distantPast
let persistedDate = conflict.persistedSnapshot?[key] as? Date ?? .distantPast
let latestUpdateAt = [sourceObjectDate, objectDate, cachedDate, persistedDate].max()
let persistedDateIsLatest = persistedDate == latestUpdateAt
let sourceObj = conflict.sourceObject
if let context = sourceObj.managedObjectContext {
context.performAndWait {
context.refresh(sourceObj, mergeChanges: !persistedDateIsLatest)
}
}
}
try super.resolve(optimisticLockingConflicts: itemConflicts)
}
}
My first question is if this code makes sense at all. I am asking this because merging conflicts are hard to test.
Specifically, I have apparently to use any of the standard merging properties in super.init(merge: .overwriteMergePolicyType)
, although is is apparently not important which one, since I am using custom merge conflict resolution.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
问题中的代码是错误的:
它过滤掉非
Item
对象的第一个冲突,并为它们调用 super 。这是正确的。然后它循环遍历
Item
对象的冲突来解决它们。在那里,它首先应用默认合并策略 (
super
),然后如果持久快照是最新的,则在完成合并的上下文中刷新对象。这是错误的原因之一是持久快照可能为零。正确的解决方法需要:
updatedAt
的属性(可以保存在源对象、对象快照、缓存快照或持久快照中),只有这样,冲突才能得到解决。
我现在使用的正确实现是:
这里,
newestSnapShot
是一个NSMergeConflict
扩展:The code in the question is wrong:
It filters out first conflicts for non-
Item
objects, and calls super for them. This is correct.Then it loops over conflicts for
Item
objects to resolve them.There, it first applies the default merge policy (
super
) and then refreshes the object in the context where merging is done if the persistent snapshot is newest. One reason why this is wrong is that the persistent snapshot can be nil.A correct resolution requires:
updatedAt
(it can be kept in the source object, the object snapshot, the cached snapshot or in the persistent snapshot),Only then is the conflict resolved.
The correct implementation that I am using now is:
Here,
newestSnapShot
is anNSMergeConflict
extension: