Correct implementation of parent/child NSManagedObjectContext

Ben Packard picture Ben Packard · Jan 11, 2013 · Viewed 17.6k times · Source

My app sometimes inserts objects into the managed object context that are not meant to necessarily be saved. For example, when I launch an 'add entity' modal, I create a managed object and assign it to the modal. If the user saves from that modal, I save the context. If he cancels, I delete the object and no save is necessary.

I have now introduced an 'import' feature that switches to my app (using a URL scheme) and adds an entity. Because one of these modals might be open, it is not safe to save the context at this point. The transient object created for the modal will be saved, even if the user cancels, and there is no guarantee that the deletion (from the cancel operation) will be saved later - the user might quit the app.

Similarly, I can't simply save whenever my app quits. If the modal is open at that point, the temporary object will be incorrectly saved.

To address this I am attempting to use a child context, as discussed here. Having read all I could find on SO, I sill have a few questions:

  1. Which concurrency type should I be using for each context? Remember that I am not doing this for performance/threading benefits. I know I can not use NSConfinementConcurrencyType for the main context if it is to have child contexts, but I'm not sure which of the other two options is best suited. For the child context, does it need to match? Or can I even use the confinement type here? I've tried a variety of combinations and all seem to work ok, but I would like to know which is appropriate for my requirements.

  2. (side issue) Why can I only get this to work if I use a class iVar? I thought I should be able to declare the temporary context in the method where it is created, and then later refer to it using entity.managedObjectContext. But it seems to be nil by the time I come to access it? This is rectified if I instead use an iVar to hold the reference.

  3. What is the correct way or propagating the change to the main context? I have seen various comments using different block-wrapped implementations on each of the contexts. Does it depend on my concurrency type? My current version is:

    //save the new entity in the temporary context
    NSError *error = nil;
    if (![myObject.managedObjectContext save:&error]) {NSLog(@"Error - unable to save new object in its (temporary) context");}
    
    //propogate the save to the main context
    [self.mainContext performBlock:^{
        NSError *error2 = nil;
        if (![self.mainContext save:&error2]) {NSLog(@"Error - unable to merge new entity into main context");}
    }];
    
  4. When my user saves, it sends its delegate (my main view controller) a message. The delegate is passed the object that was added, and it must locate that same object in the main context. But when I look for it in the main context, it is not found. The main context does contain the entity - I can log its details and confirm it is there - but the address is different? If this is meant to happen (why?), how can I locate the added object in the main context after the save?

Thanks for any insight. Sorry for a long, multi-part question, but I thought somebody was likely to have addressed all of these issues previously.

Answer

jmstone617 picture jmstone617 · Jan 11, 2013

The parent/child MOC model is a really powerful feature of Core Data. It simplifies incredibly the age-old concurrency problem we used to have to deal with. However, as you've stated, concurrency is not your issue. To answer your questions:

  1. Traditionally, you use the NSMainQueueConcurrencyType for the NSManagedObjectContext associated with the main thread, and NSPrivateQueueConcurrencyTypes for child contexts. The child context does not need to match its parent. The NSConfinementConcurrencyType is what all NSManagedObjectContexts get defaulted to if you don't specify a type. It's basically the "I will managed my own threads for Core Data" type.
  2. Without seeing your code, my assumption would be the scope within which you create the child context ends and it gets cleaned up.
  3. When using the parent/child context pattern, you need to be using the block methods. The biggest benefit of using the block methods is that the OS will handle dispatching the method calls to the correct threads. You can use performBlock for asynchronous execution, or performBlockAndWait for synchronous execution.

You would use this such as:

- (void)saveContexts {
    [childContext performBlock:^{
        NSError *childError = nil;
        if ([childContext save:&childError]) {
            [parentContext performBlock:^{
                NSError *parentError = nil;
                if (![parentContext save:&parentError]) {
                    NSLog(@"Error saving parent");
                }
            }];
        } else {
            NSLog(@"Error saving child");
        }
    }];
}

Now, you need to keep in mind that changes made in the child context (e.g. Entities inserted) won't be available to the parent context until you save. To the child context, the parent context is the persistent store. When you save, you pass those changes up to the parent, who can then save them to the actual persistent store. Saves propogate changes up one level. On the other hand, fetching into a child context will pull data down through every level (through the parent and into the child)

  1. You need to use some form of objectWithID on the managedObjectContext. They are the safest (and really only) way to pass objects around between contexts. As Tom Harrington mentioned in the comments, you may want to use existingObjectWithID:error: though because objectWithID: always returns an object, even if you pass in an invalid ID (which can lead to exceptions). For more details: Link