1
votes

I have a multi-threaded application where I need to merge a private context to the main context which in turn is connected to the persistent storage controller.

I also have the need to create temporary objects that are NOT managed (until I later on decide to manage them).

First, I tried to create my temporary objects as follows;

NSEntityDescription *entity = [NSEntityDescription entityForName:@"User" inManagedObjectContext:myMainQueueContext];
User* user = (User *)[[User alloc] initWithEntity:entity insertIntoManagedObjectContext:nil];

After deciding to keep the object or not, I then simply;

[privateContext insertObject:user];

Before I made the application multi-threaded, this worked great, but now after having torn things apart slightly and added the multi-thread concurrency by child/parent contexts, the result is NOT as expected.

By looking at the context's "registeredObjects", I can see that my created, and now inserted, user is managed in the privateContext. After saving it, the mainContext changes accordingly and I can see that it hasChanges and that there are now one object in the registeredObjects.

But looking closer at THAT registeredObject in the mainContext, reveal that it's emptied. No contents. All attributes are nil or 0 depending on type. Hence, one would expect that this might be because of the objectId is not the same... but it is ;( It's the same object. But without contents.

I tried to get some input on this concern in a different post here, but without success.

Child context objects become empty after merge to parent/main context

Anyhow, I finally got things to work by changing how I create my objects;

User* user = [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:privateContext];

Suddenly my child objects are merged to the mainContext without loosing their contents, for reasons to me unknown, but unfortunately this has also lead to the fact that I cannot any longer create temporary unmanaged objects... ;( I read that Marcus Zarra backed my first approach when it comes to creating unmanaged objects, but that does not work with merging contexts in my multi-threaded app...

Looking forward to any thoughts and ideas -- am I the only one trying to create temporary objects in an async worker-thread, where I only want to manage/merge a subset of them up to the mainContext?

EDIT

Concrete code showing what's working, and more importantly what's NOT working;

//Creatre private context and lnk to main context..
NSManagedObjectContext* privateManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];

//Link private context to main context...
privateManagedObjectContext.parentContext = self.modelManager.mainManagedObjectContext;

[privateManagedObjectContext performBlock:^()
{
    //Create user
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"User" inManagedObjectContext:self.modelManager.mainManagedObjectContext];
    User* user = (User *)[[User alloc] initWithEntity:entity insertIntoManagedObjectContext:nil];

    [user setGuid:@"123123"];
    [user setFirstName:@"Markus"];
    [user setLastName:@"Millfjord"];

    [privateManagedObjectContext insertObject:user];

    //Debug before we start to merge...
    NSLog(@"Before private save; private context has changes: %d", [privateManagedObjectContext hasChanges]);
    NSLog(@"Before private save; main context has changes: %d", [self.modelManager.mainManagedObjectContext hasChanges]);
    for (NSManagedObject* object in [privateManagedObjectContext registeredObjects])
        NSLog(@"Registered private context object; %@", object);

    //Save private context!
    NSError* error = nil;
    if (![privateManagedObjectContext save:&error])
    {
         //Oppps
         abort();
    }

    NSLog(@"After private save; private context has changes: %d", [privateManagedObjectContext hasChanges]);
    NSLog(@"After private save; main context has changes: %d", [self.modelManager.mainManagedObjectContext hasChanges]);

    for (NSManagedObject* object in [privateManagedObjectContext registeredObjects])
        NSLog(@"Registered private context object; %@", object);
    for (NSManagedObject* object in [self.modelManager.mainManagedObjectContext registeredObjects])
        NSLog(@"Registered main context object; %@", object);

     //Save main context!
     [self.modelManager.mainManagedObjectContext performBlock:^()
     {
         //Save main context!
         NSError* mainError = nil;
         if (![self.modelManager.mainManagedObjectContext save:&mainError])
         {
              //Opps again
              NSLog(@"WARN; Failed saving main context changes: %@", mainError.description);
              abort();
         }
    }];
}];

The above does NOT work, since it create a temporary object and then insert it into context. However, this slight mod make things work, but prevent me from having temporary objects...;

    NSEntityDescription *entity = [NSEntityDescription entityForName:@"User" inManagedObjectContext:self.modelManager.mainManagedObjectContext];
    User* user = (User *)[[User alloc] initWithEntity:entity insertIntoManagedObjectContext:privateManagedObjectContext];

Hence, I'm wondering; what's the difference? There must be some difference, obviously, but I don't get it.

1

1 Answers

2
votes

As far as I can tell, this is another CoreData bug.
I can somewhat understand the "how" but not the "why" of it.

As you know, CoreData rely heavily on KVO. A managed context observe changes to its objects like a hawk.
Since your "Temporary" objects have no context, the context cannot track their changes until they are attached to it, so it does not report changes to the parent context correctly (or at all). So, the parent context will get the "committed value" of the inserted object which turns to nil as soon as you insert your object to the context using insertObject: (this is the bug I guess).

So I have devised a cunning plan :D
We will swizzle our way out of this!

introducing NSManagedObjectContext+fix.m:

//Tested only for simple use-cases (no relationship tested)
+ (void) load
{
    Method original = class_getInstanceMethod(self, @selector(insertObject:));
    Method swizzled = class_getInstanceMethod(self, @selector(__insertObject__fix:));
    method_exchangeImplementations(original, swizzled);
}

- (void) __insertObject__fix:(NSManagedObject*)object
{
    if (self.parentContext && object.managedObjectContext == nil) {
        NSDictionary* propsByName = [object.entity propertiesByName];
        NSArray* properties = [propsByName allKeys];
        NSDictionary* d = [object committedValuesForKeys:properties];
        [propsByName enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSPropertyDescription* prop, BOOL *stop) {
            if ([prop isKindOfClass:[NSAttributeDescription class]]) {
                [object setValue:[(NSAttributeDescription*)prop defaultValue] forKey:key];
            } else if ([prop isKindOfClass:[NSRelationshipDescription class]]) {
                [object setValue:nil forKey:key];
            }
        }];
        [self __insertObject__fix:object];
        [object setValuesForKeysWithDictionary:d];
    } else {
        [self __insertObject__fix:object];
    }
}

This might help you keep your code a bit more sain.

However, I would probably try to avoid this type of insertion altogether.
I don't really understand your need for inserting an object to a specific context and leaving it hanging until you decide if it is needed or not.

Wouldn't it be easier to ALWAYS insert your objects into the context (keeping the values in a dictionary if needed for extended period of time). but when you decide the object should not "hit the store", simply delete it?

(this is called weeding BTW)