0
votes

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 = "[email protected]";
    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];
1
Have you tried using performBlock: instead of performBlockAndWait? Looking at your question and the two others you linked, the only thing I noticed was that you were all using performBlockAndWait. Is there any reason you want to wait? I follow a similar pattern when using CoreData but I only ever use performBlock. - jer-k
I've tried to change to performBlock, but no changes in behavior. The reason for why I'd rather the performBlockAndWait is that I want the manager "doSomeStuff" to do something async to avoid interference with main-thread. Hence, I start of by dispatching a new async thread. Once executing, that thread in turn ask the modelManager for a private context. Hence, I figured that it's enough of async processes as it is, and we're already dispatched from main-thread and UI-interferance, so let's wait for the block to end. - Markus Millfjord
Nevertheless, I'm wondering if the fact that I'm basically nesting a main thread here might interfere? I mean, main-thread call doSomeStuff which dispatch an async worked thread, which in turn call performBlockAndWait...? One too many? This approach do differ from the ref-examples since I get/create private contexts based on which thread that is running... Most examples I've seen explicitly create a new private context and then performBlock async (from the main-thread?). Just thinking loudly... I've also put the "save" mechanism as a function and not inline-code inside the block...? - Markus Millfjord
Take a look at this question I posted awhile ago stackoverflow.com/questions/13571721/…. I linked to an article that basically lead me to figure out how to set up my core data stack. Also I personally don't like your modelManager getManagedObjectContext method. I can't say if this is your issue or not but it seems odd to me that you are storing contexts in a dictionary using the thread as a key? Seems like overkill. When you enter a new thread just spawn a context and set its parent as your main thread context. Just throwing out ideas! - jer-k
I agree... The reason for that approach was that I didn't need to add the private context as argument and pass down to the model manager (from within the performBlock), so that the modelManager had the proper context when it insertObject into the actual context. Also, just realized that when the modelManager creates a new object, it uses the main context. I updated the post with code. Thx for the link, I'll start reading ;) - Markus Millfjord

1 Answers

0
votes

After having tried everything, slowly reducing my existing code to nothing, just a straight in-line version without functions nor managers -- just to pinpoint WHEN I can see that the registeredObjects in the mainContext is, after saving the child-context, actually NOT an empty element... I finally found the answer.

It turns out, that I'm creating (on purpose) objects that are NOT managed (put in context) at creation. My idea was that when I comm with a REST-API/backend, I could convert JSON responses into objects (hanging loose, without context - yet), and then compare with what I already had stored in my model manager, so that I could detect changes and notify the user on those changes...

Hence, I did;

//Create object "hanging" so that we can add it to context alter on...
NSEntityDescription *entity = [NSEntityDescription entityForName:@"User" inManagedObjectContext:mainContext];
User* user = (User *)[[User alloc] initWithEntity:entity insertIntoManagedObjectContext:nil];

... and then, when I decided that this was new stuff and things I wanted to keep;

//Insert it
[privateContext insertObject:user];

However, the inserted object was, when saved in the child context, automatically merged up to the parent's main context -- emptied! But when I tried to add the object directly to the private context, the object suddenly did NOT get emptied during merge;

//Create object AND insert into context now...
NSEntityDescription *entity = [NSEntityDescription entityForName:@"User" inManagedObjectContext:mainContext];
User* user = (User *)[[User alloc] initWithEntity:entity insertIntoManagedObjectContext:privateContext];

I don't understand why, really, but there it is. The reason for why the merged object got emptied when merging from the child's private context up to the parent's main context. I'd rather that this was NOT the case, since this will imply large restructuring of the "change-detection/notification"-code that I'm running, but at least I'm thread-safe when it comes to CoreData ;)