What do delete rules do, and what happens when you set an attribute to a value of the wrong type?

13.10.3 · L3 · Interview

Predict first

What does a cascade delete rule do?

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 container = makeContainer()
let ctx = container.viewContext
let owner = NSEntityDescription.insertNewObject(forEntityName: "Person", into: ctx)
owner.setValue("Ann", forKey: "name")
for n in ["Rex", "Tom"] {
    let pet = NSEntityDescription.insertNewObject(forEntityName: "Pet", into: ctx)
    pet.setValue(n, forKey: "name"); pet.setValue(owner, forKey: "owner")
}
try! ctx.save()
let pets = NSFetchRequest<NSFetchRequestResult>(entityName: "Pet")
print("pets before:", try! ctx.count(for: pets))
ctx.delete(owner); try! ctx.save()
print("pets after deleting the owner (cascade):", try! ctx.count(for: pets))
  • pets before: 2 / pets after deleting the owner (cascade): 0
  • pets before: 2 / pets after deleting the owner (cascade): 2
  • It fails with a validation error

Predict first

What happens when a string is set on an integer attribute?

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 container = makeContainer()
let p = NSEntityDescription.insertNewObject(forEntityName: "Person", into: container.viewContext)
p.setValue("not a number", forKey: "age")
print("unreachable")
  • The process crashes with an Objective-C exception
  • save() throws a Swift error
  • The value is converted to 0

Mental model. A delete rule is the will: when the owner goes, cascade says the estate goes with them, nullify says the dependants are freed, and deny says nothing happens while dependants remain.

Each relationship has a delete rule that says what happens to the objects on the other side when this object is deleted. Cascade deletes them too (deleting the owner deletes its pets: the check goes from 2 pets to 0), Nullify clears the inverse reference and leaves them (the usual choice for a “belongs to” side), Deny refuses to delete while related objects exist, and No Action leaves dangling references (your job to repair). Pick cascade only for true ownership; a cascade across a large graph deletes a lot and loads objects to do it, so for big purges prefer a batch delete plus explicit cleanup. Always set inverse relationships, because Core Data relies on them to keep both sides consistent and to apply delete rules correctly.

The model is checked dynamically, so key-value coding errors surface at runtime. Setting an attribute to a value of the wrong type (a string into an integer attribute) does not produce a failed save: it raises an Objective-C exception at the setValue call (NSInvalidArgumentException, “Unacceptable type of value for attribute”), which Swift cannot catch, so the process crashes. Validation rules such as required attributes and value ranges are checked at save, and report errors there. Use generated typed subclasses (or @NSManaged properties) so the compiler catches wrong types, keep entity and attribute names in constants, and run migrations against real sample data. In SwiftData the same questions appear as @Relationship(deleteRule:), and the type safety comes from the macro and the compiler.

Cascade deletes the owner’s pets along with it.

Key-value coding rejects the wrong type immediately with an exception.

Common traps

  • Using cascade on a relationship that is not ownership. Deleting one object quietly deletes much more.
  • Skipping inverse relationships. Consistency and delete rules then misbehave.

They might follow up with

  • What does Deny do on save? The save fails with a validation error if related objects exist.
  • Why does a type mistake crash instead of throwing? It is an Objective-C exception from key-value coding, not a Swift 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.4 Which parts of a SwiftData context’s lifecycle need care: autosave timing, explicit saves, rollback, and what…
  • 13.3.1 What are the parts of the Core Data stack, what is a managed object context, and what is a fault?
  • 13.4.1 How do SwiftData’s @Model, ModelContainer, ModelContext, #Predicate and FetchDescriptor work together?

← What is a fault, what does object uniquing guarantee, and why does a…   Which parts of a SwiftData context’s lifecycle need care: autosave… →