Two contexts change the same object and both save. What does each merge policy do, and what is the default?
Predict first
Two contexts save the same person. What does each policy produce?
import CoreData
func makeModel() -> NSManagedObjectModel {
let m = NSManagedObjectModel()
let person = NSEntityDescription(); person.name = "Person"; person.managedObjectClassName = "NSManagedObject"
let name = NSAttributeDescription(); name.name = "name"; name.attributeType = .stringAttributeType; name.isOptional = true
let age = NSAttributeDescription(); age.name = "age"; age.attributeType = .integer64AttributeType; age.defaultValue = 0
let pet = NSEntityDescription(); pet.name = "Pet"; pet.managedObjectClassName = "NSManagedObject"
let pname = NSAttributeDescription(); pname.name = "name"; pname.attributeType = .stringAttributeType; pname.isOptional = true
let owner = NSRelationshipDescription(); owner.name = "owner"; owner.destinationEntity = person
owner.minCount = 0; owner.maxCount = 1; owner.deleteRule = .nullifyDeleteRule
let pets = NSRelationshipDescription(); pets.name = "pets"; pets.destinationEntity = pet
pets.minCount = 0; pets.maxCount = 0; pets.deleteRule = .cascadeDeleteRule
owner.inverseRelationship = pets; pets.inverseRelationship = owner
person.properties = [name, age, pets]; pet.properties = [pname, owner]
m.entities = [person, pet]
return m
}
func makeContainer(url: URL? = nil) -> NSPersistentContainer {
let c = NSPersistentContainer(name: "T", managedObjectModel: makeModel())
let d = NSPersistentStoreDescription()
if let url { d.type = NSSQLiteStoreType; d.url = url } else { d.type = NSInMemoryStoreType }
c.persistentStoreDescriptions = [d]
c.loadPersistentStores { _, e in if let e { fatalError("\(e)") } }
return c
}
let tmp = FileManager.default.temporaryDirectory.appendingPathComponent("cd-\(UUID().uuidString).sqlite")
let container = makeContainer(url: tmp)
// Two contexts load the same person, change it, and save one after the other.
func scenario(_ policy: NSMergePolicy?, label: String, first: String, second: String) {
let a = container.newBackgroundContext(), b = container.newBackgroundContext()
if let policy { a.mergePolicy = policy; b.mergePolicy = policy }
var id: NSManagedObjectID!
a.performAndWait {
let p = NSEntityDescription.insertNewObject(forEntityName: "Person", into: a)
p.setValue("Ann", forKey: "name"); p.setValue(30, forKey: "age")
try! a.save(); id = p.objectID
}
var objA: NSManagedObject!, objB: NSManagedObject!
a.performAndWait { objA = try! a.existingObject(with: id) }
b.performAndWait { objB = try! b.existingObject(with: id) }
a.performAndWait { objA.setValue(first, forKey: "name"); objA.setValue(31, forKey: "age"); try! a.save() }
var result = "saved"
b.performAndWait {
objB.setValue(second, forKey: "name")
do { try b.save() } catch { result = "conflict error \((error as NSError).code)" }
}
let check = container.newBackgroundContext()
check.performAndWait {
let o = try! check.existingObject(with: id)
print(label, "->", result, "| name:", o.value(forKey: "name")!, "age:", o.value(forKey: "age")!)
}
}
scenario(nil, label: "default (error) policy", first: "Alice", second: "Annie")
scenario(.mergeByPropertyObjectTrump, label: "object trump", first: "Alice", second: "Annie")
scenario(.mergeByPropertyStoreTrump, label: "store trump", first: "Alice", second: "Annie")
scenario(.overwrite, label: "overwrite", first: "Alice", second: "Annie")
try? FileManager.default.removeItem(at: tmp)
- default: conflict error 133020, store keeps Alice 31 / object trump: Annie 31 / store trump: Alice 31 / overwrite: Annie 30
- All policies produce the same result
- default: saved
Mental model. Two editors take a copy of the same article. Object trump is “the last editor’s changes win line by line”; store trump is “whatever is already published wins”; overwrite is “replace the whole article with my copy”; the default is “stop and tell the editors to sort it out”.
Core Data uses optimistic locking: each context remembers the row’s state when it loaded it and, at save time, the store checks whether the row changed in between. If it did, there is a merge conflict and the context’s mergePolicy decides the result. The default is NSErrorMergePolicy: the save fails (the check reports a conflict error, code 133020) and you must handle it. The other policies resolve it automatically, and the check shows the three most used on the same scenario (A saved “Alice” with age 31 first, B then saves “Annie” without touching age): mergeByPropertyObjectTrump merges per property, and the in-memory object (B) wins where B changed something: B’s name “Annie” is kept and A’s age 31 survives, because B never changed it; mergeByPropertyStoreTrump is the mirror image, so the store’s value (“Alice”) wins on conflicting properties while B’s non-conflicting changes still apply; overwrite (NSOverwriteMergePolicy) lets the in-memory object replace the row wholesale, also reverting properties it did not change.
Choose deliberately: for a UI edit where the user’s latest input should win use object trump; for background sync where the server is the truth use store trump; keep the default error policy for data where silent merging would be wrong and handle the conflict in code (reload, ask, retry). Set the policy on every context you create (it is not inherited from the container; viewContext needs it too), keep contexts short-lived to make conflicts rare, and remember that policies work per property, so changes to different properties of one object merge cleanly while changes to the same property are the real conflict. For derived values (counters) a merge policy cannot know the intent: use an increment-style batch update or serialise the writes.
The default policy refuses; object trump keeps B’s changed name and A’s age; store trump keeps the store’s name; overwrite also reverts age.
Common traps
- Leaving the default policy on a context that saves in the background. The save fails on the first conflict.
- Assuming policies are set on the container. Each context has its own
mergePolicy.
They might follow up with
- What about changes to different properties? The property-level policies merge them; only the same property conflicts.
- How do I see conflicts instead of resolving them silently? Keep the error policy and read
NSConstraintConflictor the merge conflict info from the error.
Related questions
- 13.10.2 What is a fault, what does object uniquing guarantee, and why does a batch delete leave your objects out of…
- 13.10.3 What do delete rules do, and what happens when you set an attribute to a value of the wrong type?
- 13.3.2 How do you use Core Data across threads: contexts, perform, merging changes and merge policies?
- 13.3.3 How do Core Data migrations work: lightweight, staged and heavyweight, and what about CloudKit?
What is a fault, what does object uniquing guarantee, and why does a… →