I have an application that will sync with a server with data that can change daily. During the sync, I remove all the data for some entities and reload it with new data. I am using the following code:
func SyncronizeUserComments(theData : [[AnyHashable : Any]])
{
// Delete User Comments for this User and Connection
let commentRequest : NSFetchRequest<NSFetchRequestResult> = PT_UserComments.fetchRequest()
commentRequest.predicate = NSPredicate(format: "connection = %@ AND user == %@", Global_CurrentConnection!, Global_CurrentUser!)
coreData.processDeleteRequest(request: commentRequest)
// ADD the Comments to CoreData
for index in 0..<theData.count {
let result : [AnyHashable : Any] = theData[index]
if let commentID = result["Comment_ID"] as? String, let commentText = result["Comment_Text"] as? String, let commentTitle = result["Comment_Title"] as? String
{
let newUserComment = PT_UserComments(context: coreData.persistentContainer.viewContext)
newUserComment.connection = Global_CurrentConnection
newUserComment.user = Global_CurrentUser
newUserComment.comment_ID = commentID
newUserComment.comment_Text = commentText
newUserComment.comment_Title = commentTitle
}
}
// Add the User Comments
print("Added New User Comments: \(theData.count)")
coreData.saveContext()
}
func processDeleteRequest(request : NSFetchRequest<NSFetchRequestResult>)
{
let deleteRequest = NSBatchDeleteRequest(fetchRequest: request)
deleteRequest.resultType = .resultTypeObjectIDs
do {
let result = try coreData.persistentContainer.viewContext.execute(deleteRequest) as? NSBatchDeleteResult
let objectIDArray = result?.result as? [NSManagedObjectID]
let changes = [NSDeletedObjectsKey : objectIDArray]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes as Any as! [AnyHashable : Any], into: [coreData.persistentContainer.viewContext])
} catch {
fatalError("Fatal Error Deleting Data: \(error)")
}
coreData.saveContext()
}
When I call coreData.saveContext() I will get a Merge Conflict against the deleted data.
In reading about CoreData and the NSBatchDeleteRequest, this deletes at the SQL LITE level and bypasses the in memory cache.
The only way I have been able to get this to work is by setting:
context.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
Is this correct, or am I doing something wrong? I am also setting this merge policy in my saveContext() in the Core Data Stack.
I just spent hours debugging the same issue, hopefully this can help someone.
The problem is that NSManagedObjectContext.mergeChanges(fromRemoteContextSave:, into:)
updates the managed object context but does not update the row cache version number of the deleted objects relationships to match the updated version number (Z_OPT
) in the database file, causing a mismatch at time of the save.
If you're using NSErrorMergePolicyType
this will cause the next save to fail, (or even a later one when the relationships become flagged for save), even though everything but the version numbers match. I've not seen this mentioned in the related docs or WWDC video, but I guess Apple assumed people would always pick a non-default merge policy.
So picking NSMergeByPropertyStoreTrumpMergePolicy
solves it, as mentioned in the question, but you might not want this policy for all your save operations. To avoid that I ended up writing a custom merge policy that only resolves version mismatches. The code is below (this is untested Swift as I originally wrote in Obj-C, but should be equivalent):
//Configure the merge as below before saving
context.mergePolicy = AllowVersionMismatchMergePolicy(merge: .errorMergePolicyType)
//...
//The custom merge policy
class AllowVersionMismatchMergePolicy: NSMergePolicy {
override func resolve(optimisticLockingConflicts list: [NSMergeConflict]) throws {
do {
//if the default resolve worked leave it alone
return try super.resolve(optimisticLockingConflicts: list)
} catch {
//if any of the conflict is not a simple version mismatch (all other keys being equal), fail
let hasValueConflict = list.contains { conflict -> Bool in
//compare object and row cache
if let objectSnapshot = conflict.objectSnapshot as NSObject?,
let cachedSnapshot = conflict.cachedSnapshot as NSObject? {
return !objectSnapshot.isEqual(cachedSnapshot)
}
//compare row cache and database
if let cachedSnapshot = conflict.cachedSnapshot as NSObject?,
let persistedSnapshot = conflict.persistedSnapshot as NSObject? {
return !cachedSnapshot.isEqual(persistedSnapshot)
}
//never happens, see NSMergePolicy.h
return true
}
if hasValueConflict {
throw error
}
//Use store rollback merge policy to resolve all the version mismatches
return try NSMergePolicy.rollback.resolve(optimisticLockingConflicts: list)
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With