Tuesday, September 8, 2009

Core Data Migration Problems?

More iPhone 3 Development is going to have a brief discussion of migrations. We're going to use automatic migrations between chapters when we add to or change the data model. We're not going to do be discussing the more complex manual migrations, as Apple covers that topic pretty well in Core Data Model Versioning and Data Migration Programming Guide.

Working on the book, I discovered that there is a real gotcha in using migrations with Core Data on the iPhone. So far as I can tell, the way that you need to change your project for migrations isn't documented anywhere. It may be in there somewhere, but it certainly doesn't jump out at you.

After you create the first new version of your data model, the first thing you have to do (if using automatic migrations) is enable the automatic migrations in your persistent store coordinator. This step actually is documented, and it involves just creating an NSDictionary and passing it in when you create your persistent store coordinator. The modifications to the accessor method that was created for you automatically in your application delegate are in bold below:

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {

if (persistentStoreCoordinator != nil) {
return persistentStoreCoordinator;
}


NSURL *storeUrl = [NSURL fileURLWithPath: [[self applicationDocumentsDirectory] stringByAppendingPathComponent: @"Foo.sqlite"]];

NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil
]
;

NSError *error = nil;
persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:options error:&error]) {

NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}


return persistentStoreCoordinator;
}

From reading the documentation, it looks like that's all you have to do. Since you're done, you dutifully fire up your application and get…
Can't merge models with two different entities named 'Foo'
Well, Frack.

Here's what's going on. The .xcdatamodel classes don't get copied into your application bundle as-is. Instead, each one gets compiled into a new file of type .mom (managed object model). When you add a new version to your project, the current version gets compiled into a .mom file in your application's bundle in the Resources folder. It doesn't stop there, thought. It also creates a folder (technically, a bundle) in Resources named after your data model but with a .momd extension. Inside the .momd bundle, it puts a compiled .mom file for each of the other versions of your data model. This is understandable. It needs this information to do the automatic migration.

Here's the problem, though. The default managedObjectModel accessor method that gets created on your application delegate creates a managed object model using a factory method called mergedModelFromBundles:. This method iterates through all the files in your application's bundle looking for all .mom. It iterates the directory structure, even going down into folders and bundles. Here's what the method looks like as created for you:

- (NSManagedObjectModel *)managedObjectModel {

if (managedObjectModel != nil) {
return managedObjectModel;
}


managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];
return managedObjectModel;
}

The managedObjectModel hmethod is completely unaware of the fact that a .momd folder contains older versions of the same data model, and so it attempts to merge every version of your data model into a single merged model. Since different versions of the same data model typically have the same entities, the merge fails and you get an error message similar to the one above because entities must be unique. You can't have too "foo" entities in a single data model.

Here's the step that seems to have been missed in the documentation1. If you want to use migrations, you have to manually create your data model from the .momd bundles. Yep, can create a data model by providing a path to the .momd bundle instead of to a .mom file, and that's what you should be doing if you're going to use versioning. Here's what the new version might look like if you've got a single data model:

- (NSManagedObjectModel *)managedObjectModel {

if (managedObjectModel != nil) {
return managedObjectModel;
}


NSString *path = [[NSBundle mainBundle] pathForResource:@"Foo" ofType:@"momd"];
NSURL *momURL = [NSURL fileURLWithPath:path];
managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:momURL];

return managedObjectModel;
}

NSManagedObjectModel requires a URL to manually specify the location of a file, so we first get the path from NSBundle, then use it to create a file URL, then use that to load the data model file.

With this new version in place, automatic migrations should work, and you should be set up to do manual migrations based on the instructions in Apple's documentation.


1 - In Apple's defense, the documentation does say you can do this, it just doesn't make it clear that you need to.



45 comments:

David Green said...

Thanks for the post. I'm just about to deal with CoreData migrations. Your post has likely saved me sweat and tears since I'm also using mergedModelFromBundles:.

Jack Nutting said...

Jeff, are you sure this is really the case? I saw something similar happening (on Mac, not on iPhone), and realized that the problem was that the old model file, from before I switched on multi-versioning, was still in the app wrapper; doing a clean build eliminated that, and then the standard method of loading all model files seemed to do the right thing, just taking the new version.

Again, this was on Mac, not iPhone, so it may be slightly different.

Jeff LaMarche said...

Jack:

I'm pretty sure, but I'll investigate.

Jeff

Mozilla By said...

Great thanks for this invaluable post! I've tried everything but @"momd" :)

Bill Lindmeier said...

I was having this same problem and it turned out to be Jack's scenario. Once I removed the old model from my build, I stopped getting this error.

mhp said...
This comment has been removed by the author.
mhp said...

Any idea why i would be getting nil back from getting the "momd" path string?

Switch` said...

Same as Bill, Clean All fixed the error for me.

This was a case of a single .xcdatamodel file getting second version, thus putting both in a .xcdatamodeld. The change in new model was just couple new attributes in one of the entities.

Crunky said...

I'm getting a migraine from migrating....bahdump bump....

ah seriously, thanks for writing about this problem. I've been having a problem the last two days and trying to resolve this. I've used your code but my app still keeps crashing.

It definitely is the 2nd version of the xcdatamodel that I created that is not mapping correctly. When I delete it and go back to the original the app runs fine.

I followed your code replacing foo with my file name, I've cleaned all targets as suggested by an earlier commenter but no joy.

Everything compiles perfectly with no errors but app just keeps crashing.

I get the following error msg in the console -
reason = "Can't find or automatically infer mapping model for migration"

Could it have anything to do with the versionhashModifier? It says (null) in the console.

Do you have any idea what else it could be? Any help would be greatly appreciated.

Crunky said...

Ok, I found a solution to my problem above and it makes no sense to me. But it works.

If anybody can explain my solution to me please let me know.

In version 2 of my xcdatamodel file one of the properties is a named selldate. In the attributes section I select the Type: Date above this are three check boxes.

One for Optional, Transient and Indexed.

When I set up these properties I don't check these boxes or if they are checked I uncheck them.

Now here is the weird part. I was just changing stuff to see what kind of effect it would have because I didn't really know what else to do.

I found when I checked the Optional checkbox my app worked perfectly. No more crashes. When I unchecked the Optional box it goes back to crashing.

So the moral of the story.... when in doubt just change stuff and hope for a little bit o luck.... and check this blog to put you on the right path at least. even if you still don't know where you're going or how to get there.

jason said...

Crunky: could it have something to do with no default being set for Date or because your previous dataset didnt have this attribute no valid date was migrated causing a fault

Dario said...

Thanks for the post.
Very, Very, Very helpful !

Dario

dstites said...

Thanks for the post. It solved my problem quite nicely.

aswift said...

The mergedModelFromBundles: method works with versioned model bundles that include older model versions - the problem you encountered is (as others have mentioned) that your built resources for the project included a stale, non-versioned model file (.mom) that was also picked up by the mergedModel... method. All you need to do is make clean to address this.

AC said...

Found this while googling ... Just wanted to say that a clean build addressed it for me.

marry said...

Blogs are so informative where we get lots of information on any topic. Nice job keep it up!!
_____________________________

Shakespeare Dissertation

Shizam said...

Very helpful, thanks!

Tod Cunningham said...

Thank you thank you thank you. You just saved me hours of work.

ryemac3 said...

OK, so it's not the merging of the models that's giving me problems. It's the sqlite databases that's associated with them.

I modelled mine after Apple's Core Data Recipe app and updated the methods you showed in your tutorial.

The problem is "recipes.xcdatamodel" isn't the thing that's changed, it "Recipes.sqlite" that has new/changed entries.

How to I merge those changes to the user's preexisting database?

Santthosh said...

Awesome, this was a great insight and fixed my problem

Paul said...

Just to say that building clean didn't help for me. But Jeff has saved the day with this code snippet.

Thanks so much Jeff!

Moshe Gottlieb said...

Thanks!
I read through Apple's doc and didn't realize why I couldn't create a managed object after upgrading the data model.
Your post helped me allot!

Dissertation Writing service said...

This kind of information is very limited on internet. Nice to find the post related to my searching criteria. Your updated and informative post will be appreciated by blog loving people.

Dissertation writing services

chris said...

I copied and pasted the code into my AppDelegate.m file, only changing the file name to correspond with my app's data.

and got this error....

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSURL initFileURLWithPath:]: nil string parameter'

a clean build did not help....any ideas?

Edwin said...

scrub m65 kamagra attorney lawyer body scrub field jacket lovegra marijuana attorney injury lawyer

Steven said...

Thank you so so so much. Die apple. Die.

Jon said...

Lifesaver. Thanks!

Dissertation Writing Help said...

well i like your post . .the information in your post is quite good;) . .nice work .. Thanks a lot keep sharing:)

Dissertation Writing Services

1996Shiraz said...

Loading the momd fixed my problem, I read the documentation and it is not obvious was needed to be done. I tried the clean and rebuild and it did not resolve my merge problem. Thank you for posting this explanation of the issue, I really appreciate it.

sentaidigital.com said...

iPhone poses an interesting problem. it seems bundle-level .mom files are not removed when a new version of the app is installed. Only by deleting the app from the phone and installing it again can the bundle-level .mom file be removed leaving the .momd bundles. Of course, when you delete the app, you delete the database you were trying to migrate, too.

The trick is to *never* have bundle-level .mom files. You can do this by adding the xcdatamodeld directory to the Target Membership, but not the .xcdatamodels. (Right click "Groups & Files" to enable the "Target Membership" column.)

What a pain.

valladis said...

Thank You so match for that post. Exactly the problem I have run into.

Martin said...

This method worked for me, whereas none of the posted alternatives methods did in my case. Thanks for the great post.

iheariam said...

Wow, Jeff you come to the rescue once again, thank you for this valuable information

淺光 said...

Thank you for this, you save me a lot time.

When I saw the error message about merging models, I was suspecting that the code was erroneously trying to merge two versions, but I just don't know Core Data enough to get around it. Now I did your code and the problem was solved.

Really a good job! Thank you!

Soch said...

Added a single attribute and had to set it to Optional as the previous db didn't have it, other wise was getting "=Can't find or automatically infer mapping model for migration," segmentation fault.

chrispix said...

Thanks Jeff. I had this same problem, though in my case I have 2 models--a read-only app data store I update with version releases, and a read-write user data store that's updated with usage. So I needed to create the models then merge them using modelByMergingModels:


-(NSManagedObjectModel *)managedObjectModel {
if (managedObjectModel != nil) {
return managedObjectModel;
}

NSString *userPath = [[NSBundle mainBundle] pathForResource:@"user-data" ofType:@"momd"];
NSURL *userMomdURL = [NSURL fileURLWithPath:userPath];
NSManagedObjectModel *userModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:userMomdURL];

NSString *appDataPath = [[NSBundle mainBundle] pathForResource:@"app-data" ofType:@"momd"];
NSURL *appMomdURL = [NSURL fileURLWithPath:appDataPath];
NSManagedObjectModel *appModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:appMomdURL];

managedObjectModel = [[NSManagedObjectModel modelByMergingModels:[NSArray arrayWithObjects:userModel,appModel,nil]] retain];
[userModel release];
[appModel release];

return managedObjectModel;
}

King Brian said...

If you get this error:

Can't find or automatically infer mapping model for migration

You can see more about why in the userInfo dictionary. For me, it was changing the type of a attribute, something that is not implicitly supported.

Willow said...

Hi,

Thanks very much for this post, it solved my problem. I do find it very strange that Apple has not documented the change you suggested. Some of their documentation is severely lacking.

Thanks for making this comprehensive post.

Cheers
Willow

zsbking said...

To those who say: "Just Clean - that fixes everything"
Great, except something that simple has been attempted and doesn't solve the issue. Hence the post!

Thanks for this post!

skywalker said...

Hi,

thanks for the great post...
I have a problem..
I want to release an update for my app.

I'm stuck on how to update the existing core data database which also stores data entered by the user. All i need to do is update a couple of records and preserve current user data. No changes are made to the model.

Thanks.

Raffay said...

You have a well-written article here, very informative. Very well written I will be bookmarking this blog and subscribing to your feed so i can regularly read more articles of this quality.


reseller hosting | reseller web hosting

Ned said...

Excellent. Your re-write of managedObjectModel got me past the crash on persistentStoreCoordinator.

Unfortunately, I am now getting a "Can't find model for source store" error when calling addPeristentStoreWithType.

Any ideas?

Thanks,

Ned

Drew McKinney said...

For those receiving the *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSURL initFileURLWithPath:]: nil string parameter', and you're like me, using a new version of XCode, try looking in the package contents for your file. I found one called "CoreData.mom". So I simply replaced the path string line with this:

NSString *path = [[NSBundle mainBundle] pathForResource:@"CoreData" ofType:@"mom"];

Anyway, thanks for a great post - something that I thought would be a major headache turned out to be a few minute fix :)

Drew McKinney said...

Drew again - an amendment to my last post. I used "momd" instead of "mom". Don't do "mom" or things go funny.

NSString *path = [[NSBundle mainBundle] pathForResource:@"CoreData" ofType:@"momd"];

tmrog said...

I just spent many hours trying to get the the laptimer tutorial on core data migration to work and this fixed it. I'm not sure I would have ever figured this out on my own.

Great job!