Skip to content

Core Data Migration and Versioning

2008 July 28

By default, a Core Data application will refuse to load data created using an older data model. So, if you’ve been using one version of your model for ages and have loads of data, it’s a bit of a pain that should you wish to add an entity, attribute or relationship or make other changes to your model, you won’t be able to open the old data in a new version of your application.

Prior to OS X 10.5 if you wanted to convert data from one model version to another, you had to handle the conversion code entirely yourself. With Leopard, however, Apple built in versioning and migration capabilities to make dealing with data migration a great deal easier.

This article looks at how you can support data migration in your Core Data applications. It shows how to handle changes entirely using Data Model versions and Mapping Models, before looking at customizing the process by using custom migration classes.

Data Migration Concepts

When making a change to a data model in Xcode, it’s tempting just to open up the existing model, make that change and save the file. In order to take advantage of versioning, however, you need to have saved the previous version into your project by using a bundle for the managed object models.

A managed object model bundle allows your project to store multiple versions of the same data model, specifying which is the current version. It also allows storage of mapping models; a mapping model translates the old version of the model to the new version by specifying which source entities map to which destination entities, similarly which attributes match and also relationships. It even allows you to specify custom mapping objects that let you run code to fill out information that doesn’t just map straight across.

If you’ve enabled mapping in your application and have the right object and mapping models available, Core Data will handle the conversion transparently to the user when opening pre-existing data stores. A version hash is maintained with each entity; upon opening a persistent store, this version hash is checked against that in the current data model version. If it doesn’t match, previous models are checked until a suitable model is found, along with the mapping to convert the data.

Enabling Data Migration

In order to take advantage of the migration capabilities, you need to change the code that configures the persistent store so that it enables automatic migration. In a Core Data non-document-based application, you typically change the method that returns the persistent store coordinator such that it enables versioning when the coordinator is created by specifying NSMigratePersistentStoresAutomaticallyOption in the options for the addPersistentStoreWithType: method. In a document-based application, the NSPersistentDocument class offers a method configurePersistentStoreCoordinatorForURL: that you can override to set the NSMigratePersistentStoresAutomaticallyOption.

There’s some great sample code in an article written by Marcus Zarra for InformIT available at:
http://www.informit.com/articles/article.aspx?p=1178181&seqNum=7

Marcus demonstrates enabling migration in both document-based and non-document-based Core Data applications. For our demo application, though, we’ll use a document-based application so that it’s easy to make copies of different data versions for the migration demonstration. ***Note that if you use his code for a non-document-based app, there’s a small error in the persistentStoreCoordinator: method that the optionsDictionary mutable dictionary is created but not passed to the relevant addPersistentStoreWithType: method (nil is passed instead) meaning that migration is never enabled. Be sure to replace options:nil with options:optionsDictionary.***

The Demo Project

We’ll start by writing a simple Core Data application with a standard data model. We’ll then create a new version of the model and a mapping model to handle migration of our old data. Finally we’ll change the new version of the model such that it requires some code to migrate the old data; we’ll write two different versions of a subclass of NSEntityMigrationPolicy demonstrating two different ways to handle the migration.

The Basic Core Data Application

The basic application maintains a list of people, storing their full name along with some notes on each person:

  • Begin by creating a Core Data document-based application in Xcode.
  • Open up the data model and create a ‘Person’ entity with a ‘fullName’ string attribute and a ‘notes’ string attribute.
  • Open MyDocument.nib in Interface Builder and add an NSArrayController called ‘People’. Set its Mode to ‘Entity’ and Entity Name to ‘Person’. Tick the ‘Prepares Content’ checkbox. Bind its Managed Object Context to the File’s Owner ‘managedObjectContext’.
  • Make the interface look something like this:
    The Interface Builder layout of the document window for the example application
    Here, I’ve used an NSTableView for the list of people with its one column bound to the fullName key, along with buttons to add and remove new people; there is a text field bound to the selected person’s full name and a text view (rich text disabled) for the notes.
  • Build and run the application to make sure it works as it should. Be sure to save a sample data file containing several people so that we have something to reopen later in the tutorial.

Enabling Migration for the Application

To enable migration for a document-based application, you need to override the method configurePersistentStoreCoordinatorForURL: so that it enables the migration option:

  • Open MyDocument.m and add the following method which checks to see whether any options are currently set; if not, it creates a mutable newOptions dictionary and enables the NSMigratePersistentStoresAutomaticallyOption. Otherwise it copies the existing dictionary to gain a mutable version before enabling the option and calling the overridden configurePersistentStoreCoordinatorForURL: method, supplying the new dictionary:
    - (BOOL)configurePersistentStoreCoordinatorForURL:(NSURL *)url ofType:(NSString *)fileType modelConfiguration:(NSString *)configuration storeOptions:(NSDictionary *)storeOptions error:(NSError **)error
    {
    	NSMutableDictionary *newOptions;
     
    	if( storeOptions )
    	{
    		newOptions = [storeOptions mutableCopy];
    	}
    	else
    	{
    		newOptions = [[NSMutableDictionary alloc] init];
    	}
     
    	[newOptions setObject:[NSNumber numberWithBool:YES] forKey:NSMigratePersistentStoresAutomaticallyOption];
     
    	BOOL result = [super configurePersistentStoreCoordinatorForURL:url ofType:fileType modelConfiguration:configuration storeOptions:newOptions error:error];
     
    	[newOptions release];
    	return result;
    }
  • Build and run the project to make sure everything is still ok. Reopen the data file you created before — hopefully nothing has changed visibly as all we’ve done is enable versioning.

Adding a new Data Model version

We’ll now add a new version of the data model by using Data Model Bundles:

  • In your Xcode project browser, click once on the MyDocument.xcdatamodel file to select it.
  • Choose the ‘Design -> Data Model -> Add Model Version’ menu command and you’ll see that the MyDocument.xcdatamodel becomes MyDocument.xcdatamodeld with a little triangle to expand its contents.
  • Expand the bundle and rename the MyDocument 2.xcdatamodel file to ‘MyDocument1.0.xcdatamodel’ to distinguish between the two data models. You’ll also see that MyDocument.xcdatamodel has a little green tick in its icon to tell you that this is the current model verison.
  • Open up the current model MyDocument.xcdatamodel and add a ‘salary’ decimal attribute to the Person entity.
  • Back in the project browser, right click on the data model bundle and choose ‘Add -> New File…’. Choose the ‘Mapping Model’ template and call the file ‘MyDocument1.0.xcmappingmodel’. You’ll then be asked to choose Source and Destination models; select the MyDocument1.0.xcdatamodel file and choose ‘Set Source Model’; select the MyDocument.xcdatamodel and choose ‘Set Destination Model’.
  • When you click Finish, Xcode will generate a Mapping Model to convert the source Person entity to the destination Person entity. It will also try to map attributes for you. If you open the generated mapping model, you’ll see that it has filled out the ‘fullName’ and ‘notes’ property mappings. As it can’t find a previous ‘salary’ attribute, the value expression for that property mapping is blank. If you wish, enter ’0′ for the value expression or just leave it blank.
  • Open the MyDocument.nib file in Interface Builder and change it so there is also a bound Salary text field. Drag a suitable NSNumberFormatter onto the field:
    Interface Builder layout for the window showing the new salary text field
  • Before proceeding further, use the Finder to find the data file that you created earlier and duplicate it so that we have a copy to use later on.
  • Build and run the application and try to open your original data file. You’ll find that it opens just fine with the fullName and notes fields containing the data they had before; the salary field will either be blank or set at 0 depending on whether you entered the value expression in the property mapping earlier.
    If you run into any problems, make sure the target tickbox in Xcode is set for the current MyDocument.xcdatamodel but not the other. Otherwise you’ll get a logged problem that your application can’t merge models with two different entities named ‘Person’.

Customizing the Migration Process

Now that we’ve seen how easy it is to migrate data after adding a new attribute, we’ll look at how to customize this behaviour by splitting the old fullName attribute into a firstName and a lastName attribute using a custom entity migration policy. There are several ways to code this functionality and we’ll cover two; the first will copy attributes manually from an old entity instance to a new one whilst the second will use all the available information in the mapping model having changed the expression value for the firstName and lastName attributes at runtime.

Manual copying of data
  • Firstly, we’ll change the current MyDocument.xcdatamodel file so that it has two fields for a person’s name. We won’t bother for this tutorial creating a new model version — typically in a release application, you wouldn’t worry about versions that you create during development, only the ones you release to the user. Rename the fullName attribute ‘firstName’ and add a ‘lastName’ string attribute to the Person entity.
  • Open MyDocument.nib in Interface Builder and change the interface so that the table view shows two columns (bound to ‘firstName’ and ‘lastName’), change the fullName text field to bind to ‘firstName’ and add a second bound to ‘lastName’.
  • Back in Xcode, delete the existing MyDocument1.0.xcmappingmodel and create a new one in the same way as before. You’ll see that Xcode has left the firstName and lastName properties blank as it can’t determine how to make the right mapping.
  • Create a new Objective-C class in your Xcode project called ‘PersonMigrationClass’.
  • In the header file for that class change the inheritance so that we subclass NSEntityMigrationPolicy.
  • Implement the following method which will get called during the migration process. It migrates the Person entity by creating a new managed object in the destination managed object context before manually copying the source entity instance’s attributes into the destination entity’s attributes. Note that at the end of the method we call associateSourceInstance: withDestinationInstance: so that the internal magic can happen ready for subsequent migration steps that set up relationships etc:
    - (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)sInstance entityMapping:(NSEntityMapping *)mapping manager:(NSMigrationManager *)manager error:(NSError **)error
    {
    	NSManagedObject *newObject = [NSEntityDescription insertNewObjectForEntityForName:[mapping destinationEntityName] inManagedObjectContext:[manager destinationContext]];
     
    	NSArray *nameStrings = [[sInstance valueForKey:@"fullName"] componentsSeparatedByString:@" "];
    	[newObject setValue:[nameStrings objectAtIndex:0] forKey:@"firstName"];
    	[newObject setValue:[nameStrings objectAtIndex:1] forKey:@"lastName"];
     
    	NSString *notes = [sInstance valueForKey:@"notes"];
     
    	[newObject setValue:notes forKey:@"notes"];
     
    	[manager associateSourceInstance:sInstance withDestinationInstance:newObject forEntityMapping:mapping];
     
    	return YES;
    }
  • Back in the mapping model, click on the PersonToPerson entity mapping and on the right of the window set the ‘Custom Policy’ to ‘PersonMigrationClass’. You’ll see that the type of mapping changes to ‘Custom’.
  • Again, make a copy of the very first data file that you created so that we can use it later. Get rid of the more recent version.
  • Build and run the application and open your original data file. Hopefully, you’ll find that the first and last name fields are correctly filled out.
Another way to copy objects

It’s annoying in the previous version that we have to copy across manually the ‘notes’ attribute when we’ve already setup that attribute in the mapping model. Here we’ll implement a method that maps the entity using the mapping model’s attribute mapping, changing only the value expression for the firstName and lastName attributes. We’ll also define a ‘sourceVersion’ user info tag so that we can use the same class to handle any future versions of the data model changes:

  • Open up the MyDocument1.0.xcmappingmodel and click on the PersonToPerson entity mapping. In the far right of the window, click the ‘User Info’ tab and add a key called ‘sourceVersion’ with value ’1.0′:
    The Xcode mapping modeler showing the User Info tab
  • Change the implementation in our PersonMigrationClass to the following; this code checks the user info ‘sourceVersion’ value and, assuming it is ’1.0′ cycles through the provided attributeMappings array and sets only the ‘firstName’ and ‘lastName’ mappings. The overridden method is then called to handle all other mapping for us:
    - (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)sInstance entityMapping:(NSEntityMapping *)mapping manager:(NSMigrationManager *)manager error:(NSError **)error
    {
    	float sourceVersion = [[[mapping userInfo] valueForKey:@"sourceVersion"] floatValue];
     
    	if( sourceVersion == 1.0 ) // need to convert fullName into firstName and lastName strings
    	{
    		NSArray *nameStrings = [[sInstance valueForKey:@"fullName"] componentsSeparatedByString:@" "];
    		NSString *firstName = [nameStrings objectAtIndex:0];
    		NSString *lastName = [nameStrings objectAtIndex:1];
     
    		NSArray *attributeMappings = [mapping attributeMappings];
     
    		int count;
    		for( count = 0; count < [attributeMappings count]; count ++ )
    		{
    			NSPropertyMapping *currentMapping = [attributeMappings objectAtIndex:count];
     
    			if( [[currentMapping name] isEqualToString:@"firstName"] )
    				[currentMapping setValueExpression:[NSExpression expressionForConstantValue:firstName]];
    			else if( [[currentMapping name] isEqualToString:@"lastName"] )
    				[currentMapping setValueExpression:[NSExpression expressionForConstantValue:lastName]];
    		}
    	}
     
    	return [super createDestinationInstancesForSourceInstance:sInstance entityMapping:mapping manager:manager error:error];
    }
  • If you build and run the application and then open your original data file, it will again open with the information correctly populated but this time by using the mapping model value expressions for everything other than the two mappings we needed to customize.

Some Considerations

Notice that whilst both the above customization methods work as desired, they are potentially quite expensive in terms of processing. On a large dataset with many entities all using custom mapping in this way, the conversion could take quite a long time. There are also lots of objects floating around in memory that wouldn’t get deallocated until the next cycle through the event loop. You might consider customizing the migration process further by splitting it into multiple passes and using memory allocation pools.

Share
6 Responses leave one →
  1. December 22, 2008

    Remember to clean all your targets before trying again, to avoid the can’t merge models with two different entities named ‘Person’. error

  2. March 23, 2011

    This blog is the only place I could find detailed information on exactly how to migrate between different core data models. It is absolute gold. Thank you very very much.

  3. February 16, 2012

    I love you :)

    Best description of this i’ve found anywhere.

Trackbacks and Pingbacks

  1. Websites tagged "versioning" on Postsaver
  2. User links about "coredata" on iLinkShare
  3. Core Data Migration : 61355

Leave a Reply

Note: You can use basic XHTML in your comments. Your email address will never be published.

Subscribe to this comment feed via RSS