What are the parts of the Core Data stack, what is a managed object context, and what is a fault?

13.3.1 · L3 · Interview

Predict first

An inserted object, saved, and one with a missing mandatory attribute:

import CoreData

func makeModel(withNickname: Bool = false) -> NSManagedObjectModel {
    let m = NSManagedObjectModel()
    let e = NSEntityDescription()
    e.name = "Person"
    e.managedObjectClassName = "NSManagedObject"
    let n = NSAttributeDescription()
    n.name = "name"
    n.attributeType = .stringAttributeType
    n.isOptional = false
    var props = [n]
    if withNickname {
        let k = NSAttributeDescription()
        k.name = "nick"
        k.attributeType = .stringAttributeType
        k.isOptional = true
        props.append(k)
    }
    e.properties = props
    m.entities = [e]
    return m
}
let container = NSPersistentContainer(name: "T", managedObjectModel: makeModel())
let desc = NSPersistentStoreDescription()
desc.type = NSInMemoryStoreType
container.persistentStoreDescriptions = [desc]
container.loadPersistentStores { _, e in if let e { print("load error", e) } }
let ctx = container.viewContext
let p = NSEntityDescription.insertNewObject(forEntityName: "Person", into: ctx)
p.setValue("Asha", forKey: "name")
print(ctx.hasChanges, p.objectID.isTemporaryID)
try ctx.save()
print(ctx.hasChanges, p.objectID.isTemporaryID)
_ = NSEntityDescription.insertNewObject(forEntityName: "Person", into: ctx)
do { try ctx.save() } catch { print("failed", (error as NSError).code == NSValidationMissingMandatoryPropertyError) }
ctx.rollback()
print(ctx.hasChanges)
  • true true, false false, failed true, false
  • false false, true true, failed false, true
  • true false, false false, failed true, true

Predict first

Are fetched objects faults, and what happens when a property is read?

import CoreData

func makeModel(withNickname: Bool = false) -> NSManagedObjectModel {
    let m = NSManagedObjectModel()
    let e = NSEntityDescription()
    e.name = "Person"
    e.managedObjectClassName = "NSManagedObject"
    let n = NSAttributeDescription()
    n.name = "name"
    n.attributeType = .stringAttributeType
    n.isOptional = false
    var props = [n]
    if withNickname {
        let k = NSAttributeDescription()
        k.name = "nick"
        k.attributeType = .stringAttributeType
        k.isOptional = true
        props.append(k)
    }
    e.properties = props
    m.entities = [e]
    return m
}
let container = NSPersistentContainer(name: "T", managedObjectModel: makeModel())
let desc = NSPersistentStoreDescription()
desc.type = NSInMemoryStoreType
container.persistentStoreDescriptions = [desc]
container.loadPersistentStores { _, e in if let e { print("load error", e) } }
let ctx = container.viewContext
let p = NSEntityDescription.insertNewObject(forEntityName: "Person", into: ctx)
p.setValue("Asha", forKey: "name")
try ctx.save()
ctx.reset()
let r = try ctx.fetch(NSFetchRequest<NSManagedObject>(entityName: "Person"))
print(r.count, r[0].isFault)
_ = r[0].value(forKey: "name")
print(r[0].isFault)
  • 1 true, then false
  • 1 false, then false
  • 1 true, then true

Mental model. A context is a draft notebook copied from the library (the store). Fetching returns index cards (faults); the full page is only copied in when you read it. Saving files your edits back, all at once.

The stack has four parts. The managed object model (NSManagedObjectModel, the .xcdatamodeld) describes entities, attributes and relationships. The persistent store is where data lives (SQLite by default; also binary, or in-memory for tests). The persistent store coordinator connects model and store(s). The managed object context (NSManagedObjectContext) is the working area you actually use: you create, change and delete managed objects in it, and save() writes all pending changes to the store atomically. NSPersistentContainer builds the whole stack in a few lines and gives you viewContext (main queue, for UI) and newBackgroundContext(). A newly inserted object has a temporary object id and reports hasChanges on its context; after save() the id becomes permanent and the context is clean. Validation happens at save: a non-optional attribute left unset makes save() throw NSValidationMissingMandatoryPropertyError, and rollback() discards the pending changes.

A fault is a placeholder object: Core Data does not load an object’s data until you touch it. A fetch returns managed objects that are faults (isFault is true); reading a property fires the fault and loads the row into the context’s row cache. This keeps memory low (fetch ten thousand objects without loading ten thousand rows), and relationships are faults too, which is why accessing them in a loop causes one query each (the N+1 problem). Control it with fetchBatchSize (load in pages), relationshipKeyPathsForPrefetching, propertiesToFetch and returnsObjectsAsFaults, and use NSFetchedResultsController to drive table and collection views with change notifications. Predicates run in SQLite where possible, so keep them simple and index the attributes you filter on. Treat managed objects as belonging to their context’s queue: touch them only inside perform (or on the main queue for viewContext), and pass NSManagedObjectIDs, not objects, between contexts.

Pending changes mark the context dirty and the new object has a temporary id. After saving both settle. The incomplete object fails validation, and rollback clears it.

A fetched object starts as a fault. Reading an attribute fires the fault and loads its data.

Common traps

  • Looping over objects and reading a relationship on each. Each access fires a fault and a query (N+1); prefetch the relationship.
  • Passing managed objects between threads. They belong to a context’s queue; pass the object id and refetch.

They might follow up with

  • When does data reach disk? On save() of the context, which writes pending changes in one transaction.
  • What does a fault save you? Memory and queries: the row is only loaded when you access it.

Related questions

  • 13.3.2 How do you use Core Data across threads: contexts, perform, merging changes and merge policies?
  • 13.2.1 How do SQLite transactions, journal modes (WAL) and constraints work, and which defaults surprise people?
  • 13.4.1 How do SwiftData’s @Model, ModelContainer, ModelContext, #Predicate and FetchDescriptor work together?

How do you use Core Data across threads: contexts, perform, merging… →