Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to automatically setup Core Data relationship when using nested contexts

I'm struggling to figure out a decent solution to a problem that arises when using nested Managed Object Contexts in Core Data. Take a model that has two enites, Person and Name, where each Person has a one-to-one relationship with a Name, and Name's person relationship is not optional. Previously, in Person's -awakeFromInsert method, I would automatically create a Name entity for the new Person:

- (void)awakeFromInsert
{
    [super awakeFromInsert];

    NSManagedObjectContext *context = [self managedObjectContext];
    self.name = [NSEntityDescription insertNewObjectForEntityForName:@"Name" inManagedObjectContext:context];
}

This works just fine in a single, non-nested managed object context. However if the context has a parent context, when the child context is saved, a new Person object is created in the parent context, and -awakeFromInsert is called again on this new object before the original Person's properties and relationships are copied over. So, another Name object is created, then "disconnected" when the existing name relationship is copied over. The save fails because validation of the floating Name's now-nil person relationship fails. This problem is described here as well as in other places.

So far, I've been unable to come up with a good solution to this problem. Lazily creating the relationship in the getter method actually causes the same problem, because the getter is called by internal Core Data machinery when the new Person is created in the parent context.

The only thing I can come up with is to forgo automatic relationship generation, and always create the relationship explicitly either in the controller class that creates the Person, or in a convenience method (e.g. +[Person insertNewPersonInManagedObjectContext:]) that is only called by my code, and is always the method used to create a new Person object explicitly. Perhaps this is the best solution, but I'd prefer to not have to be so strict about only allowing a single method to be used to create managed objects, when other creation methods that I have no control over and whose use I can't easily check for/exclude, exist. For one thing, it will mean multiple NSArrayController subclasses to customize the way they create managed objects.

Has anyone else that has encountered this problem come up with an elegant solution that allows for one NSManagedObject to create a relationship object automatically upon creation/insertion?

like image 233
Andrew Madsen Avatar asked Mar 12 '13 05:03

Andrew Madsen


2 Answers

The first thought that comes to mind is that while Name's person relationship is non-optional, you didn't say that Person's name relationship is also non-optional. Is it OK to create a Person with no Name, dispose with your code, and then create the Name later on when you actually need it?

If not, one simple way is just to check whether you're on the root context before creating a Name:

- (void)awakeFromInsert
{
    [super awakeFromInsert];

    NSManagedObjectContext *context = [self managedObjectContext];
    if ([context parentContext] != nil) {
        self.name = [NSEntityDescription insertNewObjectForEntityForName:@"Name" inManagedObjectContext:context];
    }
}

But that only works if you always create new instances on a child context and you never nest contexts more than one level deep.

What I'd probably do instead is create a method like the insertNewPersonInManagedObjectContext: that you describe. Then supplement it with something like the following to handle any cases where instances get created for you (i.e. array controllers):

- (void)willSave
{
    if ([self name] == nil) {
        NSManagedObjectContext *context = [self managedObjectContext];
        Name *name = [NSEntityDescription insertNewObjectForEntityForName:@"Name" inManagedObjectContext:context];
        [self setName:name];
    }
}

...and of course don't bother with a custom awakeFromInsert...

like image 67
Tom Harrington Avatar answered Oct 20 '22 03:10

Tom Harrington


I ended up going with the convenience method solution. All the NSManagedObject subclasses in my app have a +insertInManagedObjectContext: method. Creating instances of those objects (in my own code) is always done using that method. Inside that method, I do this:

+ (instancetype)insertInManagedObjectContext:(NSManagedObjectContext *)moc
{
    MyManagedObject *result = [NSEntityDescription insertNewObjectForEntityForName:@"MyEntityName" inManagedObjectContext:moc]
    [result awakeFromCreation];
    return result;
}

- (void)awakeFromCreation
{
    // Do here what used to be done in -awakeFromInsert.
    // Set up default relationships, etc.
}

As for the NSArrayController issue, solving that isn't bad at all. I simply created a subclass of NSArrayController, overrode -newObject, and used that subclass for all the relevant NSArrayControllers in my app:

@implementation ORSManagedObjectsArrayController

- (id)newObject
{
    NSManagedObjectContext *moc = [self managedObjectContext];
    NSEntityDescription *entity = [NSEntityDescription entityForName:[self entityName]
                                              inManagedObjectContext:moc];
    if (!entity) return nil;

    Class class = NSClassFromString([entity managedObjectClassName]);
    return [class insertInManagedObjectContext:moc];
}

@end
like image 23
Andrew Madsen Avatar answered Oct 20 '22 04:10

Andrew Madsen