Correct implementation of parent/child NSManagedObjectContext
Asked Answered
P

3

27

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.

Pearle answered 11/1, 2013 at 18:20 Comment(0)
M
47

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
Mutiny answered 11/1, 2013 at 20:19 Comment(9)
Based on Andy's answer above, I tested a version without using blocks for saving and so far it is working (not to say that it's correct). What is the purpose of using the block implementation, and what problems might arise if I continue to simply call save on the contexts ?Pearle
As for #2, which I suppose shouldn't belong in the question really - I still have a valid reference to my inserted object - isn't it keeping its moc alive?Pearle
As I said in #3 above, the purpose of using the block-based methods is to ensure that your messages get dispatched to the correct threads. This is really only important if you're doing multithreaded core data work. As an aside, you can also use performBlock to asynchronously do work (if the MOC is on a different thread. Calling performBlock on a MOC on the main thread just dispatches the code to the main thread)Mutiny
thats valid, of course you need to take care of your threads. If you, for example, want to save the parent context within a child contexts block, then you need to use a seperate block for it. But if you want to save the parent context outside of the childs block, you dont need to use a block, because you are back on the main thread.Croquette
Be careful! If you perform changes on the child context, the parent won't know about them unless you save! I am updating my answer to include this.Mutiny
Thanks - I'm aware - in fact that's a key behavior I am trying to achieve from all of this - not updating the main context with the new entity until it is confirmed by the user. Worth mentioning explicitly though.Pearle
A good answer. I'd suggest that instead of objectWithID: that you use existingObjectWithID:error: though-- because objectWithID: always an object, even if you pass in an invalid ID (which can lead to exceptions). Better to be sure that you only get objects already known to exist.Jailbird
Thanks @TomHarrington. I have updated my answer to include this.Mutiny
What if you make changes to the parent context and want them to propagate to the children?Crapshooter
C
6
  1. If you use the parent/child pattern, you usually declare the parent context with NSMainQueueConcurrencyType and the child contexts with NSPrivateQueueConcurrencyType. NSConfinementConcurrencyType is used for the classic threading pattern.

  2. If you want to keep the context, you somehow need a strong reference to it.

  3. You simply call the save method on the child context to push the changes to the parent context, if you want to persist the data, you call save on the parent context as well. You dont need to do this within a block.

  4. There are several methods to get an specific object from a context. I cant tell you which one will work in your case, try them out:

    - objectRegisteredForID:

    - objectWithID:

    - existingObjectWithID:error:

Croquette answered 11/1, 2013 at 19:40 Comment(2)
Thanks - any background on why I don't need to use blocks? Seen conflicting info elsewhere.Pearle
If you're using one of the queue based concurrency options, you really need to use the block calls to be safe. Skipping them will often work but not always. The only exception is if you used NSMainQueueConcurrencyType and you are making the call from the main queue. In that case, it's optional.Jailbird
S
6

I have had similar problems and here are answers to some parts of your questions- 1. You should be able to use concurrency type NSPrivateQueueConcurrencyType or NSMainQueueConcurrencyType 2. Say you have created a temporary context tempContext with parent context mainContext (this is assuming iOS5). In that case, you can just move your managed object from tempContext to mainContext by-

object = (Object *)[mainContext objectWithID:object.objectID];

You can then save the mainContext itself.

Maybe also,

[childContext reset];

if you want to reset the temporary context.

Squirmy answered 11/1, 2013 at 19:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.