如何实施 CoreData 自定义合并策略?

发布于 2025-01-10 11:30:38 字数 3082 浏览 2 评论 0原文

我的应用程序使用 CoreData + CloudKit 同步。一些 CoreData 实体(例如 Item)可以通过 iCloud 的共享数据库进行共享。该应用仅使用 1 个 NSPersistentContainer,但它有 2 个 NSManagedContextsvisualContext 和一个 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

つ低調成傷 2025-01-17 11:30:38

问题中的代码是错误的:

它过滤掉非 Item 对象的第一个冲突,并为它们调用 super 。这是正确的。

然后它循环遍历 Item 对象的冲突来解决它们。
在那里,它首先应用默认合并策略 (super),然后如果持久快照是最新的,则在完成合并的上下文中刷新对象。这是错误的原因之一是持久快照可能为零。

正确的解决方法需要:

  • 首先找到最新的updatedAt的属性(可以保存在源对象、对象快照、缓存快照或持久快照中),
  • 存储这些属性,
  • 应用自定义合并策略所基于的默认合并策略,
  • 如果需要,将对象属性设置为存储的最新值。

只有这样,冲突才能得到解决。

我现在使用的正确实现是:

override func resolve(optimisticLockingConflicts list: [NSMergeConflict]) throws {
    for conflict in list {
        let sourceObject = conflict.sourceObject
        // Only UpdateTimestampable objects can use the custom merge policy. Other use the default merge policy.
        guard sourceObject is UpdateTimestampable else {
            try super.resolve(optimisticLockingConflicts: [conflict])
            continue
        }
        let newestSnapshot = conflict.newestSnapShot
        
        if let sourceObject = sourceObject as? Item {
            let fixedAtTopAt: Date?
            let howOftenBought: Int32
            let lastBoughtDate: Date?
            let name: String
            let namesOfBuyPlaces: Set<String>?
            let status: Int16
            let updatedAt: Date?
            
            let sourceObjectUpdatedAt = sourceObject.updatedAt ?? .distantPast
            if sourceObjectUpdatedAt >= newestSnapshot?["updatedAt"] as? Date ?? .distantPast {
                fixedAtTopAt = sourceObject.fixedAtTopAt
                howOftenBought = sourceObject.howOftenBought
                lastBoughtDate = sourceObject.lastBoughtDate
                name = sourceObject.name
                namesOfBuyPlaces = sourceObject.namesOfBuyPlaces
                status = sourceObject.status
                updatedAt = sourceObject.updatedAt
            } else {
                fixedAtTopAt = newestSnapshot?["fixedAtTopAt"] as? Date
                howOftenBought = newestSnapshot?["howOftenBought"] as! Int32
                lastBoughtDate = newestSnapshot?["lastBoughtDate"] as? Date
                name = newestSnapshot?["name"] as! String
                namesOfBuyPlaces = newestSnapshot?["namesOfBuyPlaces"] as? Set<String>
                status = newestSnapshot?["status"] as! Int16
                updatedAt = newestSnapshot?["updatedAt"] as? Date
            }
            // Here, all properties of the newest Item or Item snapshot have been stored.
            // Apply now the default merging policy to this conflict.
            try super.resolve(optimisticLockingConflicts: [conflict])
            // Overwrite now the source object's properties where necessary
            if sourceObject.fixedAtTopAt != fixedAtTopAt { sourceObject.fixedAtTopAt = fixedAtTopAt }
            if sourceObject.howOftenBought != howOftenBought { sourceObject.howOftenBought = howOftenBought }
            if sourceObject.lastBoughtDate != lastBoughtDate { sourceObject.lastBoughtDate = lastBoughtDate }
            if sourceObject.name != name { sourceObject.name = name }
            if sourceObject.namesOfBuyPlaces != namesOfBuyPlaces { sourceObject.namesOfBuyPlaces = namesOfBuyPlaces }
            if sourceObject.status != status { sourceObject.status = status }
            if sourceObject.updatedAt != updatedAt { sourceObject.updatedAt = updatedAt }
            continue
        } // source object is an Item
        
        if let sourceObject = conflict.sourceObject as? Place {
            // code for Place object …
        }
    }
}  

这里,newestSnapShot 是一个 NSMergeConflict 扩展:

extension NSMergeConflict {
    var newestSnapShot: [String: Any?]? {
        guard sourceObject is UpdateTimestampable else { fatalError("must be UpdateTimestampable") }
        let key = Schema.UpdateTimestampable.updatedAt.rawValue
        /* Find the newest snapshot.
         Florian Kugler: Core Data:
         Note that some of the snapshots can be nil, depending on the kind of conflict you’re dealing with. 
         For example, if the conflict occurs between the context and the row cache, the persisted snapshot will be nil. 
         If the conflict happens between the row cache and the persistent store, the object snapshot will be nil. 
         */
        let objectSnapshotUpdatedAt = objectSnapshot?[key] as? Date ?? .distantPast
        let cachedSnapshotUpdatedAt = cachedSnapshot?[key] as? Date ?? .distantPast
        let persistedSnapshotUpdatedAt = persistedSnapshot?[key] as? Date ?? .distantPast
        if persistedSnapshotUpdatedAt >= objectSnapshotUpdatedAt && persistedSnapshotUpdatedAt >= cachedSnapshotUpdatedAt {
            return persistedSnapshot
        }
        if cachedSnapshotUpdatedAt >= persistedSnapshotUpdatedAt && cachedSnapshotUpdatedAt >= objectSnapshotUpdatedAt {
            return cachedSnapshot
        }
        if objectSnapshotUpdatedAt >= persistedSnapshotUpdatedAt && objectSnapshotUpdatedAt >= cachedSnapshotUpdatedAt {
            return objectSnapshot
        }
        fatalError("No newest snapshot found")
    }
}

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:

  • to find first the properties of the latest updatedAt (it can be kept in the source object, the object snapshot, the cached snapshot or in the persistent snapshot),
  • to store these properties,
  • to apply the default merge policy on that the custom merge policy is based,
  • to set if required the objects properties to the stored newest values.

Only then is the conflict resolved.

The correct implementation that I am using now is:

override func resolve(optimisticLockingConflicts list: [NSMergeConflict]) throws {
    for conflict in list {
        let sourceObject = conflict.sourceObject
        // Only UpdateTimestampable objects can use the custom merge policy. Other use the default merge policy.
        guard sourceObject is UpdateTimestampable else {
            try super.resolve(optimisticLockingConflicts: [conflict])
            continue
        }
        let newestSnapshot = conflict.newestSnapShot
        
        if let sourceObject = sourceObject as? Item {
            let fixedAtTopAt: Date?
            let howOftenBought: Int32
            let lastBoughtDate: Date?
            let name: String
            let namesOfBuyPlaces: Set<String>?
            let status: Int16
            let updatedAt: Date?
            
            let sourceObjectUpdatedAt = sourceObject.updatedAt ?? .distantPast
            if sourceObjectUpdatedAt >= newestSnapshot?["updatedAt"] as? Date ?? .distantPast {
                fixedAtTopAt = sourceObject.fixedAtTopAt
                howOftenBought = sourceObject.howOftenBought
                lastBoughtDate = sourceObject.lastBoughtDate
                name = sourceObject.name
                namesOfBuyPlaces = sourceObject.namesOfBuyPlaces
                status = sourceObject.status
                updatedAt = sourceObject.updatedAt
            } else {
                fixedAtTopAt = newestSnapshot?["fixedAtTopAt"] as? Date
                howOftenBought = newestSnapshot?["howOftenBought"] as! Int32
                lastBoughtDate = newestSnapshot?["lastBoughtDate"] as? Date
                name = newestSnapshot?["name"] as! String
                namesOfBuyPlaces = newestSnapshot?["namesOfBuyPlaces"] as? Set<String>
                status = newestSnapshot?["status"] as! Int16
                updatedAt = newestSnapshot?["updatedAt"] as? Date
            }
            // Here, all properties of the newest Item or Item snapshot have been stored.
            // Apply now the default merging policy to this conflict.
            try super.resolve(optimisticLockingConflicts: [conflict])
            // Overwrite now the source object's properties where necessary
            if sourceObject.fixedAtTopAt != fixedAtTopAt { sourceObject.fixedAtTopAt = fixedAtTopAt }
            if sourceObject.howOftenBought != howOftenBought { sourceObject.howOftenBought = howOftenBought }
            if sourceObject.lastBoughtDate != lastBoughtDate { sourceObject.lastBoughtDate = lastBoughtDate }
            if sourceObject.name != name { sourceObject.name = name }
            if sourceObject.namesOfBuyPlaces != namesOfBuyPlaces { sourceObject.namesOfBuyPlaces = namesOfBuyPlaces }
            if sourceObject.status != status { sourceObject.status = status }
            if sourceObject.updatedAt != updatedAt { sourceObject.updatedAt = updatedAt }
            continue
        } // source object is an Item
        
        if let sourceObject = conflict.sourceObject as? Place {
            // code for Place object …
        }
    }
}  

Here, newestSnapShot is an NSMergeConflict extension:

extension NSMergeConflict {
    var newestSnapShot: [String: Any?]? {
        guard sourceObject is UpdateTimestampable else { fatalError("must be UpdateTimestampable") }
        let key = Schema.UpdateTimestampable.updatedAt.rawValue
        /* Find the newest snapshot.
         Florian Kugler: Core Data:
         Note that some of the snapshots can be nil, depending on the kind of conflict you’re dealing with. 
         For example, if the conflict occurs between the context and the row cache, the persisted snapshot will be nil. 
         If the conflict happens between the row cache and the persistent store, the object snapshot will be nil. 
         */
        let objectSnapshotUpdatedAt = objectSnapshot?[key] as? Date ?? .distantPast
        let cachedSnapshotUpdatedAt = cachedSnapshot?[key] as? Date ?? .distantPast
        let persistedSnapshotUpdatedAt = persistedSnapshot?[key] as? Date ?? .distantPast
        if persistedSnapshotUpdatedAt >= objectSnapshotUpdatedAt && persistedSnapshotUpdatedAt >= cachedSnapshotUpdatedAt {
            return persistedSnapshot
        }
        if cachedSnapshotUpdatedAt >= persistedSnapshotUpdatedAt && cachedSnapshotUpdatedAt >= objectSnapshotUpdatedAt {
            return cachedSnapshot
        }
        if objectSnapshotUpdatedAt >= persistedSnapshotUpdatedAt && objectSnapshotUpdatedAt >= cachedSnapshotUpdatedAt {
            return objectSnapshot
        }
        fatalError("No newest snapshot found")
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文