0

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];
Community
  • 1
  • 1
Markus Millfjord
  • 707
  • 1
  • 6
  • 26
  • 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 Feb 12 '14 at 00:36
  • 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 Feb 12 '14 at 14:49
  • 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 Feb 12 '14 at 14:53
  • Take a look at this question I posted awhile ago http://stackoverflow.com/questions/13571721/creating-background-thread-for-core-data-writing. 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 Feb 12 '14 at 17:00
  • 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 Feb 12 '14 at 17:44

1 Answers1

0

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 ;)

Markus Millfjord
  • 707
  • 1
  • 6
  • 26
  • A quick reflection; I would say that this is a bug in CoreData? Apple say themselves in their documentation that it's perfectly doable to create NSManagedObjects without inserting them into context -- and that objects can be added in a later stage using the context's insertObject-function... But, my findings in this post show that such an approach cannot be combined with a multi-context app merging child contexts up to parent/main contexts since objects are emptied dying the merge. Has anyone had similar results? Is this likely to be a iOS bug, or is it "just me"? ;) – Markus Millfjord Feb 13 '14 at 19:21
  • Have you found any solution to this? I'm just fighting with exactly the same thing. It really sounds kind of stupid to have to maintain two sets of data models (one for json storage, one for coredata) - just for the sake of core data to work :( Yet, this is precisely what i am doing too and it is a pain . to do something so simple. – Martin Kovachev Dec 16 '16 at 13:24
  • Honestly, it's been a while now but I think my problems disaapeared when I stopped playing with migration and merge settings. The app I developed is still live and well, and has been re-released for iOS 8, 9 and 10 and we haven't had any further issues with CoreData thoughout that period of time. – Markus Millfjord Jan 03 '17 at 17:15