I’m having a Core Data issue while using 2 NSManagedObjectContext
, running on different threads, and migrating changes from the parent to the child. In essence, I’m able to pull the changes from parent to child, but immediately after doing so, the changes are lost.
The app I’m building is a test for syncing a model between multiple devices and a server.
The context that holds the objects the user interacts with is on the main thread and is configured as a child of the sync context and is created like this (error checks omitted)
NSManagedObjectContext *parentMOC = self.syncManagedObjectContext;
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext performBlockAndWait:^() {
[_managedObjectContext setParentContext:parentMOC];
}];
The syncManagedObjectContext is the parent context and is where the syncManager performs the sync with the server. It gathers up objects modified by the user, sends the changes to the server and merges changes received back. The syncManagedObjectContext also sends its data to the PersistentStoreCoordinator
to be stored in SQLite
.The context runs on a background “thread” so that syncing and storing does not block the main thread. Here’s how it is created:
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
_syncManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_syncManagedObjectContext performBlockAndWait:^(){
[_syncManagedObjectContext setPersistentStoreCoordinator:coordinator];
}];
Sync Logic Flow
The syncing is kicked off when the syncManager handles the NSManagedObjectContextObjectsDidChangeNotification from the main context. Here is a rough flow of what happens:
- syncManager handles
NSManagedObjectContextObjectsDidChangeNotification
which lets it know that objects have been changed in the main thread context. It calls save on the main context which saves the changes to syncMOC. - When the syncManager receives
NSManagedObjectContextDidSaveNotification
to indicate the save has been completed, it gathers up the newly changed objects from the sync context and sends the changes to the server. It then does a save on the sync MOC which sends the data to SQLite. Note that each object has a uuid field which I create as a portable id - not be confused with Core Data’s objectID, as well as a lastSynced timestamp that is provided by the server. - The server responds back with updated timestamps for the objects sent, as well as any other changes that have happened. In the simplest case that illustrates the issue, what is received is a set of records that consists of the uuid and the updated lastSynced time for the objects that the syncManager just sent.
- For each update, the syncManager updates the object in the syncContext and stores the NSManagedObject objectID for the object (not the uuid) in an array.
- The syncManager then does a save on the on the sync MOC to write the data to disk and posts a message to provide the main MOC with the array of objectID’s for updated objects. At this point, if I do a fetch for all Entities in the syncMOC and dump them to the log, they all have the correct values. Further, if I look at the SQLite database on disk, it too has the correct values.
- Here’s abbreviated code (some error checking and non-essential stuff removed) for how the updates are merged in on the main thread, with comments as to what’s happening: (Note: I’ve been careful in the code to use performBlock and it appears from tracing in the debugger that everything is happening on the correct thread.)
-(void)syncUpdatedObjects: (NSNotification *)notification
{
NSDictionary *userInfo = notification.userInfo;
NSArray *updates = [userInfo objectForKey:@"updates"];
NSManagedObjectContext *ctx = self.managedObjectContext;
[ctx performBlock:^() {
NSError *error = nil;
for (NSManagedObjectID *objID in updates) {
NSManagedObject *o = [ctx existingObjectWithID:objID error:&error];
// if I print out o or inspect in the debugger, it has the correct, updated values.
if (o) {
[ctx refreshObject:o mergeChanges:YES];
// refreshObject causes o to be replaced by a fault, though the NSLog statement will pull it back.
// NOTE: I’ve used mergeChanges:NO and it doesn’t matter
NSLog(@"uuid=%@, lastSynced = %@", [o valueForKey:@"uuid”], [o valueForKey:@"lastSynced"]);
// Output: uuid=B689F28F-60DA-4E78-9841-1B932204C882, lastSynced = 2014-01-15 05:36:21 +0000
// This is correct. The object has been updated with the lastSynced value from the server.
}
}
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@“MyItem"
inManagedObjectContext:ctx];
request.entity = entity;
NSArray *results = [ctx executeFetchRequest:request error:&error];
for (MyItem *item in results)
NSLog(@"Item uuid %@ lastSynced %@ ", item.uuid, item.lastSynced);
// Output: uuid B689F28F-60DA-4E78-9841-1B932204C882 lastSynced 1970-01-01 00:00:00 +0000
// Now the objects have incorrect values!
}];
}
In case you missed it, the issue is there in the comments after the NSLog
statements. The object initially has the correct values from the parent context, but then they become incorrect. Look at the timestamp, specifically.
Does anyone have any idea why this would happen? I should note that the business of doing the fetch at the end was part of debugging. I was noticing that the NSManagedObjects being held on to in the program did not have the correct values, even though I was seeing that things were updated correctly in the above code and through uniquing, they should be updated too. I thought that what might be happening is that I was creating “extra” objects with the correct values while the old ones were still around. However, the fetch showed that the right objects and only the right ones were around, only with bad values.
One more thing, if I do the same fetch in the parent context after this function runs, it shows the correct values as does SQLite
.
Any help is much appreciated!