Core Data Derived Attributes
Documentation for Core Data’s new derived attributes feature is up!
It’s a really cool feature.
NSDerivedAttributeDescription (see also: derivationExpression):
Use derived attributes to optimize fetch performance[…]
[…]
Data recomputes derived attributes when you save a context. A managed object’s property does not reflect unsaved changes until you save the context and refresh the object.
That makes sense given that the derivation is implemented in the SQLite store using triggers. And you wouldn’t want every change to cause a fetch for properties that you might not need right away.
However, you have to be careful to manually refresh the objects that you care about because, since at least macOS 10.14, there’s a bug where NSFetchRequest
’s shouldRefreshRefetchedObjects option doesn’t work (FB6161838). It fetches the right objects, but their properties may be stale. See, for example, this code:
import Foundation import CoreData class Entity: NSManagedObject { @NSManaged var attribute: String } let attribute = NSAttributeDescription() attribute.name = "attribute" attribute.attributeType = .stringAttributeType let entityDescription = NSEntityDescription() entityDescription.name = "Entity" entityDescription.properties = [attribute] entityDescription.managedObjectClassName = Entity.className() let model = NSManagedObjectModel() model.entities = [entityDescription] let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model) // Also happens with SQLite store try! coordinator.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: [:]) let writeContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) writeContext.persistentStoreCoordinator = coordinator let readContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) readContext.persistentStoreCoordinator = coordinator let writeEntity = Entity(entity: entityDescription, insertInto: writeContext) writeContext.performAndWait { writeEntity.attribute = "Old" try! writeContext.save() } var readEntity: Entity? = nil readContext.performAndWait { let request = NSFetchRequest<Entity>(entityName: entityDescription.name!) readEntity = try! readContext.fetch(request).first! // Initially the attribute should be Old, and that's what's printed print(readEntity!.attribute) } writeContext.performAndWait { writeEntity.attribute = "New" try! writeContext.save() } readContext.performAndWait { let request = NSFetchRequest<Entity>(entityName: entityDescription.name!) request.shouldRefreshRefetchedObjects = true _ = try! readContext.fetch(request) // Now the attribute should be New, but it is still Old print(readEntity!.attribute) readContext.refresh(readEntity!, mergeChanges: false) // However, manually refreshing does update it to New print(readEntity!.attribute) }
In this example, you could instead use notifications to merge changes from one context into the other. But that wouldn’t work with derived attributes since their changes aren’t reported in notifications.