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.

No comments:

Post a Comment