Thursday, October 17, 2019

Core Data Derived Attributes

Scott Perry:

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.

Comments RSS · Twitter

Leave a Comment