Child context objects become empty after merge to

2019-09-18 20:42发布

问题:

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

回答1:

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