I have a NSManagedObject, NewOrder, that I store just the NSManagedObjectID for out of context. When I need to access it, I'm either in the main context, or in a performAndWait() block for a persistentContainer.newBackgroundContext()
on a BG thread, and I use context.existingObject(with:NSManagedObjectID)
to pull up a context-appropriate instance, and save after making changes.
For some reason, I'm getting old data after making changes. I'm positive that I've saved my changes with try! context.save()
.
As much as possible, I try to use persistentContainer.newBackgroundContext()
instead of the main context so I don't lock up the main thread, which means a good number of contexts touch this NSManagedObjectID variable. The strange thing is, it predictably happens after two newBackgroundContext calls, in the exact same place between one new context and the next. It's almost as if it is recycling a bg context that never was notified of the saved changes.
It looks something like this:
DispatchQueue.global(qos: .userInitiated).async
{
let context = persistentContainer.newBackgroundContext()
context.performAndWait
{
let newOrder = try! context.existingObject(with:self.newOrderOID) as! NewOrder
let client = try! context.existingObject(with: self.clientOID) as! Client
print(client.id)//10386
print(newOrder.userID!)// "10386"
//replacing old client object with new one, including data on the newOrder object
context.delete(client)
let newClient = Client(context: context)
self.clientOID = newClient.objectID
print(newClient.id) //10152
newOrder.userID = String(client.id)
try! context.save()
print(newOrder.userID!) //"10152"
}
}
//a bit later in the code, access again to do some reading, NO changes
let semaphore = DispatchSemaphore(value: 0)
DispatchQueue.global(qos: .userInitiated).async
{
let context = persistentContainer.newBackgroundContext()
context.performAndWait
{
let newOrder = try! context.existingObject(with:self.newOrderOID) as! NewOrder
let newClient = try! context.existingObject(with: self.clientOID) as! Client
print(newOrder.userID!)// "10152"
print(newClient.id) //10152
semaphore.signal()
}
}
semaphore.wait()
//IMMEDIATELY AFTER the previous context shows the correct userID
DispatchQueue.global(qos: .userInitiated).async
{
let context = persistentContainer.newBackgroundContext()
context.performAndWait
{
let newOrder = try! context.existingObject(with:self.newOrderOID) as! NewOrder
let newClient = try! context.existingObject(with: self.clientOID) as! Client
print(newOrder.userID!)// "10386" ????
print(newClient.id) //10152 !!
}
}
As far as my reading of the documentation has taken me, I'm under the impression that these background contexts are supposed to be rooted directly to the data store, much like the main context is, and should receive updates on every change to the data store. So if I save on one, it should show those changes on all of them. But only SOME of them are reflecting the changes: Sometimes the object won't delete from the store across all contexts, and sometimes the data in the object is old. What's particularly alarming and flummoxing is that one NEW context will have correct data, and another NEW context created after it will have completely different data.
The above example is a rough simplification of something more complicated, so it's possible the above is impossible and there's a mistake in the more complicated code, but I've been fighting this one error for almost a week with no progress except for finding the exact spot it happens, and discovering that the only change between updated data and outdated data is a new background context.
I don't know Objective-C and am about 4 months in on CoreData using Swift, and have run into nothing but trouble trying to teach it to myself. I'm sure I'm missing something here.
Does anyone know why this object would not be updating to all contexts? Am I misunderstanding something?
Changes made since question posted
As per @Jon Rose 's comment, I refactored the entire app to support doing all reading tasks on the main context and all writing tasks in a queue on a private context, and that seemed to remove the issue I was seeing.
Here's the code I added in my CoreData manager (called persistenceManager):
lazy var persistentContainerQueue = OperationQueue()
func enqueueAndSave(shouldWait:Bool = false, closure: @escaping (_ writeContext: NSManagedObjectContext) -> Void)
{
let semaphore = DispatchSemaphore(value: 0)
persistentContainerQueue.addOperation()
{
self.persistentContainer.performBackgroundTask()
{writeContext in
closure(writeContext)
self.save(writeContext)
semaphore.signal()
}
}
if shouldWait
{
semaphore.wait()
}
}
How I use it:
persistenceManager.enqueueAndSave(shouldWait: true)
{ writeContext in
let newOrder = try! writeContext.existingObject(with:self.newOrderOID) as! NewOrder
let client = try! writeContext.existingObject(with: self.clientOID) as! Client
print(client.id)//10386
print(newOrder.userID!)// "10386"
//replacing old client object with new one, including data on the newOrder object
writeContext.delete(client)
let newClient = Client(context: writeContext)
self.clientOID = newClient.objectID
print(newClient.id) //10152
newOrder.userID = String(client.id)
try! writeContext.save()
print(newOrder.userID!) //"10152"
}
//a bit later in the code, access again to do some reading, NO changes
persistenceManager.readContext.performAndWait
{
let newOrder = try! persistenceManager.readContext.existingObject(with:self.newOrderOID) as! NewOrder
let newClient = try! persistenceManager.readContext.existingObject(with: self.clientOID) as! Client
print(newOrder.userID!)// "10152"
print(newClient.id) //10152
}
//IMMEDIATELY AFTER the previous context shows the correct userID, now showing correct data :D
persistenceManager.enqueueAndSave(shouldWait: true)
{ writeContext in
let newOrder = try! writeContext.existingObject(with:self.newOrderOID) as! NewOrder
let newClient = try! writeContext.existingObject(with: self.clientOID) as! Client
print(newOrder.userID!)// "10152"
print(newClient.id) //10152
}
edit: I still encountered some problems with missing data, which seemed to be solved by putting an NSLock in the persistenceManager that is locked on every save, and unlocked when the save finishes. The getter/setter for every relevant NSManagedObject must also successfully .lock() before returning/writing to their respective variables.
edit: I'm still encountering old data when reading from the viewContext, in other areas of the app. All writes are being queued, and I've turned my viewContext variable into a computed property that will wait until the queue is empty before returning the context.
Basically, I write data on a private context and save, blocking the main thread until the write and save is complete, and then read the data from the same object I just changed by using viewContext.existingObject(with objectID: NSManagedObjectID)
, and display that data to a UILabel. That data is old data about 80% of the time. In fact, I have a hard time getting it to change, ever. The pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int)
function is the one that triggers the write.
It doesn't seem to matter whether I use self.persistentContainer.performBackgroundTask()
or
let writeContext = self.persistentContainer.newBackgroundContext()
writeContext.performAndWait(){}
I don't understand why writing on a private context to an object with a specific location in the data store, saving, passing the location in the data store to the viewContext, and the using that location to retrieve that data using that data store location would result in any other object besides the updated one. Especially if the changes are queued, and the read of the data waits for all changes to complete before proceeding. It's like the viewContext isn't getting notified of changes made by the write contexts when they save. Both the write contexts and the viewContext are sourced directly in the data store, and documentation indicates they both should be getting notifications of changes. And throughout most of the application, that's exactly what appears to happen. Except for these pickers. (So far anyways. Who knows where a similar issue might pop up again. I have completely lost confidence in the reliability of CoreData.)
It seems the queue improved, but did not solve the problem.
I'm thinking of refactoring the app to find the objects by some unique ID instead of by their objectID, but I have no confidence that will resolve the issues I'm facing.
Any advice would be appreciated.
edit:
I refactored the app, changed every entity to have a property uuid:UUID, and instead of storing the NSManagedObjectID of objects I want to access later or in a different context and using context.existingObject(with:NSManagedObjectID), I store the UUID and do a fetch request based on the UUID. When I do it this way, core data can no longer fulfill the fault for the object once the object is found. The view context thinks there should be an object in the store, but the object is not in the store when it looks. This applies to every newly created object that is created/saved on a private context, when accessed from the viewContext.