Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why doesn’t NSFetchRequest.shouldRefreshRefetchedObjects work?

I’m trying to update and save a managed object in one context and then access the updated attribute value in another context. The documentation for shouldRefreshRefetchedObjects says:

By default when you fetch objects, they maintain their current property values, even if the values in the persistent store have changed. Invoking this method with the parameter YES means that when the fetch is executed, the property values of fetched objects are updated with the current values in the persistent store. This is a more convenient way to ensure that managed object property values are consistent with the store than by using refreshObject:mergeChanges: (NSManagedObjetContext) for multiple objects in turn.

So I thought that by setting this to true I would get current values after refetching, without having to manually refresh the individual objects. However, that does not seem to be the case. On macOS 10.14.5, the fetch request will select the proper objects based on the property values in the store, but the objects in memory still have stale values.

Here’s some sample code to illustrate the problem. I expect it to print Old New New, but instead it prints Old Old New.

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)
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)
    _ = try! readContext.fetch(request)
    // However, manually refreshing and fetching again does update it to New
    print(readEntity!.attribute)
}

I’m aware of refreshAllObjects(), but that:

  1. Potentially affects many more objects that don’t need to be updated right now.
  2. Doesn’t provide control over merging changes.
  3. Posts a change notification.

shouldRefreshRefetchedObjects seems to be exactly what I want; it just doesn’t seem to do anything. The best workaround seems to be to individually refresh the objects, but I’m guessing that’s inefficient.

like image 771
Michael Tsai Avatar asked Jun 17 '19 15:06

Michael Tsai


1 Answers

The short answer is: This is a bug in the framework. The code in the question should work, but doesn't, because shouldRefreshRefetchedObjects doesn't work as advertised.

I also tried some other variations on the code. I changed it to use a SQLite persistent store so that I could turn on SQLite debugging and see if it told me anything interesting. The fetch that gets the unexpected result prints these messages in the Xcode console:

CoreData: annotation:  with values: (
    "<JunkCDCmd.Entity: 0x100704760> (entity: Entity; id: 0x9e9e44e129d3565d 
    <x-coredata://3F8EC946-9EF7-4A62-AEF2-A8670185E23C/Entity/p1>; 
    data: {\n    attribute = Old;\n})"
)

It's getting Old, which is not what's expected here. Most likely the result is coming from the managed object context's cache, which suggests that shouldRefreshRefetchedObjects is not getting checked by the framework.

I also tried a few other things that seemed unlikely to help but that I couldn't rule out without trying. I used shouldRefreshRefetchedObjects on the initial fetch, because why not. I tried keeping readEntity from the first fetch instead of letting it go out of scope. I tried nesting the writeContext.performAndWait inside the readContext.performAndWait, in case scope was relevant. I saved fetch results in variables instead of using _ to make sure the results weren't unexpectedly deallocard or something. As expected, none of these made any difference.

The bounty asks for

A thorough explanation of why in fact this does not work and how to implement it in whatever place it is that it should work.

The exact reason it doesn't work is impossible to know without the source code to the framework. Without that we can only speculate. It's clearly broken, and it looks as though it's broken because it's not checking shouldRefreshRefetchedObjects. If it's not working for you (and it looks like it can't possibly be working for you) then file a bug with Apple and hope for the best.

As for how to implement it, if this setting is exactly what you need then there is no exact alternative. Your options for refreshing values include, in no particular order

  • Forcing the object to become a fault before the fetch, as in the question, by using refresh(_:mergeChanges:) with false for the second argument. This might be awkward if you have a bunch of affected objects.
  • Refreshing the object directly by using refresh(_:mergeChanges:) with true for the second argument. You don't need to re-fetch. This might be awkward if you have a bunch of affected objects.
  • Refreshing all registered objects with refreshAllObjects(), which has the drawbacks described in the question.
  • Merging changes from an NSManagedObjectContextDidSave notification (a.k.a. Notification.Name.didSaveObjectsNotification).
like image 53
Tom Harrington Avatar answered Nov 14 '22 15:11

Tom Harrington