I am working on a multi-threaded app using Core Data. Ironically, I thought the app was close to be finished when I learnt that Core Data was not thread safe... Hence, I'm now adding multi-context instead of the single-context that I got from the Xcode template (and has been working so far, really, but that's more luck than skill I guess)
I'm attempting to use the > iOS 5.0 approach with parent/child contexts which would fit well with what I'm trying to do, but when I insert valid objects with valid data/properties in my child-context, they have all become nil, or 0 (depending on attribute type) in the parent context.
I've noticed that there are similar posts, but none with any answer;
Parent MOC get changes with empty data from child MOC
NSManagedObject values are correct, then incorrect when merging changes from parent to child NSManagedObjectContext
Here's some code to get the idea;
I have a singleton manager that the UI uses to "do stuff", and then use delegates or callbacks when mission is complete. That manager in turn has a model-manager to deal with persistent data management, and some other comm-managers to talk to the web/REST-API etc.
- (void) doSomeStuff:(NSString*)someParam
callbackObject:(NSObject*)object
onSuccess:(SEL)successSelector
onFailure:(SEL)failureSelector
{
//Kick as an async thread since we don't want to disturb the UI
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^(void)
{
//Ask model manager for a nice context to work with...
//NOTE; All contexts (private ones) are stored in a NSDictionary using currentThread as key, so that we always can access the "correct" context at anytime if we need to insert/delete/change anything in the context
NSManagedObjectContext* privateContext = [self.modelManager getManagedObjectContext];
//Perform stuff in block since our context is of NSPrivateQueueConcurrencyType
[privateContext performBlockAndWait:^{
//... do the actual stuff, go online, talk to a REST-API, wait for things, which will eventually result in a new object being created
User* user = ...;
//Store object in model manger which is my abstraction of CoreData
//NOTE: the modelManager will get a reference to the currently used privateContext and use it to insert the object
[self.modelManager addUser:user];
//Save model!
[self.modelManager save];
}];
//Trigger callback, if applicable
if (loggedInUserGuid && successSelector)
{
[object performSelectorOnMainThread:successSelector withObject:loggedInUserGuid waitUntilDone:NO];
}
});
}
The save function in the modelManager take into consideration the context concurrencyType and will behave according to spec when using child/parent contexts;
- (void) save
{
//Get current context...
NSManagedObjectContext* currentContext = [self getManagedObjectContext];
//If the current context has any changes...
if ([currentContext hasChanges])
{
//Changes detected! What kind of context is this?
switch (currentContext.concurrencyType)
{
case NSPrivateQueueConcurrencyType:
{
NSError* error = nil;
if (![currentContext save:&error])
abort();
if (self.mainManagedObjectContext hasChanges])
{
[self.mainManagedObjectContext performBlockAndWait:^{
NSError *mainError;
if (![self.mainManagedObjectContext save:&mainError])
abort();
}];
}
break;
}
....
}
}
}
By adding debug prints before and after saving the child context AND the parent/main-context, I noticed that the inserted object is nicely there in the child context, and the child context say "hasChanges == YES", whereas the main-context say "hasChanges == NO", as expected.
(entity: User; id: 0x10c06c8c0 <x-coredata:///User/tDFBBE194-44F9-44EC-B960-3E8E5374463318> ; data: {
created = nil;
emailAddress = "wilton@millfjord.se";
firstName = Wilton;
guid = "2eaa77fa-0d2c-41b8-b965-c4dced6eb54a";
lastName = Millfjord;
nbrOfOfflineKeys = 5;
password = 123456;
})
The main-context is thereafter saved as well, and looking at it's registeredObjects before saving, we can see that "hasChanges == YES" (expected after the child's save/merge of inserted objects up to it's parent. But, moreover -- all params and attributes are now nil or 0 depending on attribute type...;
(entity: User; id: 0x10c06c8c0 <x-coredata:///User/tDFBBE194-44F9-44EC-B960-3E8E5374463318> ; data: {
created = nil;
emailAddress = nil;
firstName = nil;
guid = nil;
lastName = nil;
nbrOfOfflineKeys = 0;
password = nil;
})
As you can see, the ID is the same, so it's the "same" object, but without/reset contents. I've tried all various combinations of "setMergePolicy" but no effect.
I've even tried the < iOS 5.0 approach of adding a NSNotificationCentre approach where I "mergeChangesFromContextDidSaveNotification", and the only thing I could verify there was that the data that came in the NSNotification argument was valid and "good data", but main context was still not updated properly. The result was still an empty object.
Looking forward to your thoughts and ideas.
/Markus
Update
The code used when creating new managed objects, using the main context regardless of which thread/context-performBlock that is running at the time...
NSEntityDescription *entity = [NSEntityDescription entityForName:@"User" inManagedObjectContext:self.mainManagedObjectContext];
User* user = (User *)[[User alloc] initWithEntity:entity insertIntoManagedObjectContext:nil];
The object user is thereafter hanging without context until later on when I decide to add it to managed context (using my getManagedObjectContext to ge the proper/current context).
[[self getManagedObjectContext] insertObject:user];