how to merge two different instances of NSManagedObjects in child contexts into single instance in parent context

StackOverflow https://stackoverflow.com/questions/21468242

سؤال

I'm having a race condition problem in trying to create or update managed objects in child contexts of a common parent.

Assume the following setup:

  1. a save context with NSPrivateQueueConcurrencyType. This is also the only context with a persistent store coordinator pointing to the backing SQL.
  2. a main with NSMainQueueConcurrencyType
  3. as many child contexts of the main context as needed, which are dynamically created as I fetch data from a server and use it to create or update objects in the graph.
  4. Some model entity, say, User. Assume User has 2 attributes: id and name. Also, User has a to-many relationship friends.

The flow for retrieving a user along with their attributes and relationships would go something like this (I'm simplifying and using a bit of pseudo code cause the actual implementation is lengthy and complex, but this should get the point across):

  1. You ask for user with id 1
  2. I branch into two child contexts of the main context: one will fetch attributes and the other the relationship.
  3. The context in charge of dealing with attributes does this:

    [childContext1 performBlock:^{
    
        // id jsonData = GET http://myserver.com/users/1
    
        NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:user.entity.name];
        fetchRequest.predicate = [NSPredicate predicateWithFormat:@"id = %i", [jsonData[@"id"] intValue]];
        NSArray *results = [childContext1 executeFetchRequest:fetchRequest error:&error];
        NSManagedObject *newOrExistingObject = nil;
        BOOL existsInContext = (BOOL)[results count];
        if (!existsInContext) {
            newOrExistingObject = [NSEntityDescription insertNewObjectForEntityForName:e.name inManagedObjectContext:childContext1];
        }
        else {
            newOrExistingObject = results[0];
        }
    
        // at this point, the id and name of the user would be set.
        [newOrExistingObject setValuesForKeysWithDictionary:jsonData];
        [childContext1 save:nil];
    
        [mainContext performBlock:^{
          [mainContext obtainPermanentIDsForObjects:mainContext.insertedObjects.allObjects error:nil];
          [mainContext save:nil];
          [saveContext performBlock:^{
            [self.saveContext save:nil];
          }];
        }];
    }};
    
  4. The context in charge of dealing with the relationship would do this:

    [childContext2 performBlock:^{
    
        // id jsonData = GET http://myserver.com/users/friends/
    
        NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:user.entity.name];
        fetchRequest.predicate = [NSPredicate predicateWithFormat:@"id = %i", [jsonData[@"id"] intValue]];
        NSArray *results = [childContext2 executeFetchRequest:fetchRequest error:&error];
        NSManagedObject *newOrExistingObject = nil;
        BOOL existsInContext = (BOOL)[results count];
        if (!existsInContext) {
            newOrExistingObject = [NSEntityDescription insertNewObjectForEntityForName:e.name inManagedObjectContext:childContext2];
        }
        else {
            newOrExistingObject = results[0];
        }
    
        // at this point, the 'friends' relationship would be set
        [newOrExistingObject setValuesForKeysWithDictionary:jsonData];
        [childContext2 save:nil];
    
        [mainContext performBlock:^{
          [mainContext obtainPermanentIDsForObjects:mainContext.insertedObjects.allObjects error:nil];
          [mainContext save:nil];
          [saveContext performBlock:^{
            [self.saveContext save:nil];
          }];
        }];
    }};
    

The problem with the above approach is that in order to "create or update", I need to figure out if the object in question already exists (which I attempt to do so by doing a fetch by id since this is the attribute I base equivalence on).

As it stands, sometimes one of the child contexts will be done doing its thing and by the time the other context attempts fetching an object with the id in question, it'll get the object previously created by the other context, update it and everything will go fine. But sometimes, whichever child context executes second does not find the object previously created by the child who executed first. So it creates another new object with the same id. By the time the whole thing is done and both children propagated their objects to the main context and the main context in turn to the save context, I end up with two objects with different objectID but same id: one will have the attributes and the other the relationship. My guess is this is because by the time the second context tries to fetch the object, either:

  • A: the child context which created the object is not done propagating it back to the main context by the time the other child context attempts to retrieve it.
  • B: the main context is not done assigning permanent IDs to the objects it just got from the first child context.

Basically what I need is a way to either synchronize each child context's fetch, so that this cannot happen until whichever child executes first and creates the object has saved and in turn the object has been propagated to the main context. Or, perhaps better yet, a way for the main context to know that any number of objects of the same entity class and with the same id attribute value are equivalent (despite their objectIDs being different since they were created in different contexts in parallel) and so should be merged into a single one.

Any thoughts?

هل كانت مفيدة؟

المحلول

dispatch_semaphore_t

:p

I wasn't aware that I could use GCD semaphores inside an NSManagedObjectContext's -performBlock.

I ended up creating a mutex (a binary semaphore), so that the child blocks do:

[childContext performBlock:^{

    // id jsonData = GET http://myserver.com/<whateverData>

    dispatch_semaphore_wait(_s, DISPATCH_TIME_FOREVER);

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:user.entity.name];
    fetchRequest.predicate = [NSPredicate predicateWithFormat:@"id = %i", [jsonData[@"id"] intValue]];
    NSArray *results = [childContext2 executeFetchRequest:fetchRequest error:&error];
    NSManagedObject *newOrExistingObject = nil;
    BOOL existsInContext = (BOOL)[results count];
    if (!existsInContext) {
        newOrExistingObject = [NSEntityDescription insertNewObjectForEntityForName:e.name inManagedObjectContext:childContext];
    }
    else {
        newOrExistingObject = results[0];
    }

    [newOrExistingObject setValuesForKeysWithDictionary:jsonData];
    [childContext obtainPermanentIDsForObjects:childContext.insertedObjects.allObjects error:nil];
    [childContext save:nil];

    dispatch_semaphore_signal(_s);

    [mainContext performBlock:^{
      [mainContext save:nil];
      [saveContext performBlock:^{
        [self.saveContext save:nil];
      }];
    }];
}};

And this did the trick

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top