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:
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.
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
refresh(_:mergeChanges:)
with false
for the second argument. This might be awkward if you have a bunch of affected objects.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.refreshAllObjects()
, which has the drawbacks described in the question.NSManagedObjectContextDidSave
notification (a.k.a. Notification.Name.didSaveObjectsNotification
).If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With