Previous Entry Share Next Entry
Let's merge managed object models!
latest
chanson
There was a question recently on Stack Overflow asking how to handle cross-model relationships in managed object models. Now, the poster wasn't asking about how to handle relationships across persistent stores — he was asking how to handle splitting a model up into pieces such that the pieces could be recombined.

It turns out that this is somewhat straightforward to do using Core Data. Let's say you have a simple model with Song and Artist entities. I'll write it out here in a pseudo-modeling language for ease of reading:
MusicModel = {
    Song = {
        attribute title : string;
        attribute duration : float;
        to-one-relationship artist : Artist,
            inverse : songs,
            delete-rule : nullify;
        userInfo = { };
    };

    Artist = {
        attribute name : string;
        to-many-relationship songs : Song,
            inverse : artist,
            delete-rule : cascade;
        userInfo = { };
    };
};
Now let's say you want to split this up into two models, where Song is in one and Artist is in the other. You could just try and create two xcdatamodel files in Xcode, one with each entity, and wire the relationships together after loading them and merging them with +[NSManagedObjectModel modelByMergingModels:]. Except that won't work: Relationships with no destination entity won't be compiled by the model compiler.

What else might you try? You could try just putting dummy entities in for relationships to point to. However, merging models will fail then, because NSManagedObjetModel won't merge models that have entity name collisions.

It turns out, though, that you can merge models very easily by hand, by taking advantage of the way Core Data's model-description objects handle the NSCopying protocol. All you have to do is create your destination model, loop through every entity in each of your source models, and copy every entity that you haven't tagged as a stand-in using a special key in their userInfo dictionary.

Why does this work? The trick is that before you tell a persistent store coordinator to use a model, that model is mutable and references relationship destination entities and inverse relationships by name. So you can have only a minimal representation of Artist in one model, and a minimal representation of Song in another model:
SongModel = {
    Song = {
        attribute title : string;
        attribute duration : float;
        to-one-relationship artist : Artist,
            inverse : songs,
            delete-rule : nullify;
        userInfo = { };
    };

    Artist = {
        /* Note no attributes. */
        to-many-relationship songs : Song,
            inverse : artist,
            delete-rule : cascade;
        userInfo = { IsPlaceholder = YES; };
    };
};

ArtistModel = {
    Song = {
        /* Note no attributes. */
        to-one-relationship artist : Artist,
            inverse : songs,
            delete-rule : nullify;
        userInfo = { IsPlaceholder = YES; };
    };

    Artist = {
        attribute name : string;
        to-many-relationship songs : Song,
            inverse : artist,
            delete-rule : cascade;
        userInfo = { };
    };
};
Then, when you write some code to combine them, the merged model will wind up with the full definition of Song and the full definition of Artist. Here's an example of the code you might write to do this:
- (NSManagedObjectModel *)mergeModelsReplacingDuplicates:(NSArray *)models {
    NSManagedObjectModel *mergedModel = [[[NSManagedObjectModel alloc] init] autorelease];

    // General strategy:  For each model, copy its non-placeholder entities
    // and add them to the merged model. Placeholder entities are identified
    // by a MyRealEntity key in their userInfo (which names their real entity,
    // though their mere existence is sufficient for the merging).

    NSMutableArray *mergedModelEntities = [NSMutableArray arrayWithCapacity:0];

    for (NSManagedObjectModel *model in models) {
        for (NSEntityDescription *entity in [model entities]) {
            if ([[[entity userInfo] objectForKey:@"IsPlaceholder"] boolValue]) {
                // Ignore placeholder.
            } else {
                NSEntityDescription *newEntity = [entity copy];
                [mergedModelEntities addObject:newEntity];
                [newEntity release];
            }
        }
    }

    [mergedModel setEntities:mergedModelEntities];

    return mergedModel;
}
This may seem like a bit of overhead for this simple example. The critical thing to see above is that only that which is necessary for model consistency is in the placeholder entities. Thus you only need the inverse relationship from Song to Artist in ArtistModel. Say you wanted to add a Picture entity related to the Artist entity — you don't have to add that to both models, only to ArtistModel. The benefit of this method for merging models should then be pretty apparent: It gives you the ability to make your model separable, just like your code.

  • 1

It would be nice if the model compiler did this for us

(Anonymous)
As part of the build process, the model compiler should take multiple files, merge entities with the same name by creating a union of their properties/relationships, and generated a single compiled model file.

The problem with coredata modeling is it doesn't scale across multiple developers. If two developers on a project make independent changes to a model, then one will lose and have to repeat his work. This is a major impediment to scaled development.

If its that easy, build it into the toolset. I'm sure its easier for you than for us.

EOF had a nice modelgroup concept that just handled this. We'd like that capability back, thanks.

Re: It would be nice if the model compiler did this for us

Please file an enhancement request against the developer tools using Apple's bug reporter at http://bugreport.apple.com - that's the way to ensure your request is seen by engineers and tracked.

Thanks!

Inheritance

(Anonymous)
Inheritance tree has to be recreated for copied entities. With the code provided, trying to init a persistent store against the merged model will throw an exception : NSInternalInconsistencyException, because it will not find original subentities.

Re: Inheritance

(Anonymous)
Here is what I added to workaround this problem.

NSDictionary *entitiesByName = [mergedModel entitiesByName];
for (NSEntityDescription* entity in parentEntities) {
NSMutableArray *newSubentities = [NSMutableArray array];
for (NSEntityDescription* subentity in [entity subentities]) {
NSEntityDescription *newSubentity = [entitiesByName objectForKey:[subentity name]];
[newSubentities addObject:newSubentity];
}
[[entitiesByName objectForKey:[entity name]] setSubentities:newSubentities];
}

Re: Inheritance

(Anonymous)
Please remove my previous comment with the wrong formatting.

Here is the code I have added to work around the problem. This replaces the references to the original model with references to the new model.

NSDictionary *entitiesByName = [mergedModel entitiesByName];
for (NSEntityDescription* entity in parentEntities) {
    NSMutableArray *newSubentities = [NSMutableArray array];
    for (NSEntityDescription* subentity in [entity subentities]) {
        NSEntityDescription *newSubentity = [entitiesByName objectForKey:[subentity name]];
        [newSubentities addObject:newSubentity];
    }
    [[entitiesByName objectForKey:[entity name]] setSubentities:newSubentities];	
}


Re: Inheritance

(Anonymous)
Thank you!

Couldn't find out why my merging failed. Now it does not!

/Dan

Re: Inheritance

(Anonymous)
Thanks for the addition, however there is one small "bug" in it on the third line. This code works for me.
[mergedModel setEntities:mergedModelEntities];
	NSDictionary *entitiesByName = [mergedModel entitiesByName];
	for (NSEntityDescription *entity in [mergedModel entities]) {	//cycle through all entities from merged model
		NSMutableArray *newSubentities = [NSMutableArray array];	//array holder for new subentities
		for (NSEntityDescription *subentity in [entity subentities]) {	//cycle through all subentities of entity
			NSEntityDescription *newSubentity = [entitiesByName objectForKey:[subentity name]];
			[newSubentities addObject:newSubentity];
		}
		[[entitiesByName objectForKey:[entity name]] setSubentities:newSubentities];	   //replace subentities
	}

  • 1
?

Log in