What is a fault, what does object uniquing guarantee, and why does a batch delete leave your objects out of date?
Predict first
What do faulting, uniquing and a batch delete look like?
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("cd2-\(UUID().uuidString).sqlite")
let container = makeContainer(url: tmp)
let ctx = container.viewContext
let p = NSEntityDescription.insertNewObject(forEntityName: "Person", into: ctx)
p.setValue("Ann", forKey: "name")
try! ctx.save()
let id = p.objectID
ctx.reset()
let obj = try! ctx.fetch(NSFetchRequest<NSManagedObject>(entityName: "Person"))[0]
print("after fetch isFault:", obj.isFault)
print("name:", obj.value(forKey: "name")!, "| isFault after reading:", obj.isFault)
print("same objectID in one context gives the same instance:", try! ctx.existingObject(with: id) === obj)
let other = container.newBackgroundContext()
var otherObj: NSManagedObject!
other.performAndWait { otherObj = try! other.existingObject(with: id) }
print("another context gets another instance:", otherObj !== obj)
let del = NSBatchDeleteRequest(fetchRequest: NSFetchRequest<NSFetchRequestResult>(entityName: "Person"))
del.resultType = .resultTypeObjectIDs
let res = try! ctx.execute(del) as! NSBatchDeleteResult
let ids = res.result as! [NSManagedObjectID]
print("deleted in the store:", ids.count, "| context still thinks isDeleted:", obj.isDeleted)
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: [NSDeletedObjectsKey: ids], into: [ctx])
print("after merging the deleted ids, isDeleted:", obj.isDeleted)
try? FileManager.default.removeItem(at: tmp)
- after fetch isFault: true / name: Ann | isFault after reading: false / same objectID gives the same instance: true / another context another instance: true / deleted in the store: 1 | context still thinks isDeleted: false / after merging, isDeleted: true
- isFault is false after fetch
- the batch delete updates the context automatically
Mental model. A fault is a name tag on an empty seat: you know who it is for, and the person only arrives when you speak to them. A batch delete is a cleaner removing chairs while a seating plan on someone’s desk still lists them.
A fault is a placeholder: the object exists and has an identity but its attribute data has not been loaded, so memory stays low and fetches are cheap. In the check, a fetched object reports isFault == true, and reading name “fires” the fault (one trip to the store or its row cache), after which isFault is false. Faults are why you can fetch thousands of objects without loading all their data, and why relationship traversals are lazy (fetch with relationshipKeyPathsForPrefetching and fetchBatchSize to avoid one query per object, the N+1 problem). Uniquing guarantees that within one context a given NSManagedObjectID maps to one instance (the check shows existingObject(with:) returns the same object), while another context gets its own copy; that is why you pass NSManagedObjectIDs, not objects, between contexts and threads (Q333).
Batch requests (NSBatchInsertRequest, NSBatchUpdateRequest, NSBatchDeleteRequest) are fast because they run directly in SQLite and bypass the contexts, so objects already in memory are stale: after the batch delete the store reported one deleted row, but the context’s object still reported isDeleted == false until the deleted objectIDs were merged with mergeChanges(fromRemoteContextSave:into:). Ask for .resultTypeObjectIDs, merge them into the contexts that matter (or set automaticallyMergesChangesFromParent and use persistent history), and expect that batch requests need a SQLite store (an in-memory store throws for them in this check), do not run validation or relationship rules or fire property observers, and do not update the object graph. Use them for bulk work (imports, purges), not for single edits.
Fetched objects are faults, uniquing is per context, and batch deletes need their ids merged into contexts.
Common traps
- Running a batch delete and then using objects already in memory. They are stale until you merge the changes or refresh.
- Passing managed objects between threads. Pass
NSManagedObjectIDs and re-fetch in the target context.
They might follow up with
- How do I avoid N+1 faults when rendering a list? Set
fetchBatchSizeand prefetch the relationships you will read. - Does
refresh(_:mergeChanges:)turn an object into a fault? WithmergeChanges: falseit does, dropping unsaved changes and reloading data on next access.
Related questions
- 13.10.1 Two contexts change the same object and both save. What does each merge policy do, and what is the default?
- 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.1 What are the parts of the Core Data stack, what is a managed object context, and what is a fault?
- 13.3.2 How do you use Core Data across threads: contexts, perform, merging changes and merge policies?
← Two contexts change the same object and both save. What does each… What do delete rules do, and what happens when you set an attribute… →