Video icon 64
Learning to code? Skill up faster with our practical video courses. Start your free trial today.
Advertisement

Working with Core Data: Schema Versioning and Lightweight Migrations

by
Student iconAre you a student? Get a yearly Tuts+ subscription for $45 →

After reading First steps with Core Data, we find ourselves enlightened in the way Core Data works and how it can help us develop data rich applications. However, the surface was only just scratched, and in some applications you might be left wondering how to implement a certain piece of functionality.

My Experience with Core Data

Over the past months I have been working with Core Data on a pet project. It is an application to pull in data from a remote service API, store the data in Core Data, and display it in a collection of UITableViews. Over the course of development, it has gone from a Property List based data store (*shudder*) to now a Core Data SQL store. After three complete rebuilds and many long nights of debugging, my application has finally become a fast, crash free, and memory leak free project. For me, using Core Data has made my application what it was intended to be, and I will be sharing much of what I've learned with you in this and future articles.

For this article, we will build upon the previous LapTimer application and demonstrate performing lightweight migrations.

Schema Changes, Versioning, and Migrations

When you are developing an application you will almost never get the schema right the first time. So, Core Data has support for schema versioning and data migration. This allows you to define a new version of the Core Data schema by adding a new attribute or entity to the store. Then define how the store has to migrate from one change to another.

Creating a new xcdatamodel

Core Data handles schema versions differently to some other frameworks you might have used in the past. When you go through development and require a new attribute or entity the process of adding these is by creating a new xcdatamodel file for your application. This new file will be versioned, have an incremental number in the file name, and is distinct from older schema versions. In the LapTimer application, create the new Data Model version. Click on the xcdatamodel file in your application and then navigate to the menu item at: Design > Data Model > Add Model Version.

Add Schema Version

You will now have a xcdatamodel tree, the parent being a new xcdatamodel file, with a new schema version denoted by the incremental number. Also, there is a green tick against the old schema. This indicates what schema is active and the application it will use. It is important to note that choosing the new version straight away may cause some issues in development. Since we haven't setup a migration for this new schema yet, if we were to make the new schema the active one then the Simulator or Device will error and crash on boot. As a demonstration, take a look at the error below, it is common for what people encounter on their first try:

Version Mismatch Error

It's basically just saying "Hey, you changed the active DB schema and I'm stuck on an older version." This prevents the application from conflicts and errors with the Core Data store schema changing. It does a comparison based on the details of the schema that the store is operating with. If it is given a new schema version to use but it does not match what it has been setup or migrated to, then you get this error.

So, how do we get past this? Well there are two options:

  1. If the app is in development and you don't care about any data in the application, just delete the application from the device and re-install it. It will delete the store and its contents but it will create the app with the new schema. This is only an option in development.
  2. Use lightweight migration to automatically take care of small schema changes whenever you make them. This is limited to only a few migrations which are detailed below.

Lightweight Migration

This is a easy to use and an automatic feature of Core Data, but it is limited to only simple migrations, hence the reason it is lightweight. You can only use this method if the changes are among the following:

  • Adding a new attribute/property to an entity.
  • Renaming of an entity or an attribute/property if the 'Renaming Identifier' is given.
  • Changing an attribute/property to optional or non-optional, whether it requires a value on creation.

Note: Before we dive in, if you are working on the LapTimer project you will need to build the project before we start. Make sure the active xcdatamodel is the first version that the application was built with. This way you will be able to see the migration work.

As a demonstration, the LapTimer application is going to have the Event entity changed to Lap. With this we also have to update the Event.h and Event.m classes and any occurrences of those in the application. With a couple nifty Xcode features this is painless.

So, in the new version of the xcdatamodel we created earlier, I am going to make the change by simply changing the values in the entity details panel. Now we need to set the 'Renaming Identifier' to Event. So in the same panel click on the spanner/wrench and enter "Event" into the Renaming Identifier field, like so:

Name change
Renaming Ident

The next step is to tell Core Data that it can perform lightweight migrations on boot. Referencing the LapTimer application, in the LapTimerAppDelegate.m file we will need to add some code to the -(NSPersistentStoreCoordinator *)persistentStoreCoordinator method:

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
	
    if (persistentStoreCoordinator != nil) {
        return persistentStoreCoordinator;
    }
	
    NSURL *storeUrl = [NSURL fileURLWithPath: [[self applicationDocumentsDirectory] stringByAppendingPathComponent: @"LapTimer.sqlite"]];
	
    NSError *error = nil;
    persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];

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

    if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:options error:&error]) {
		/*
		 Replace this implementation with code to handle the error appropriately.
		 
		 abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be 
                 useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
		 
		 Typical reasons for an error here include:
		 * The persistent store is not accessible
		 * The schema for the persistent store is incompatible with current managed object model
		 Check the error message to determine what the actual problem was.
		 */
		NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
		abort();
    }    
	
    return persistentStoreCoordinator;
}

The code added was the options NSDictionary which will tell Core Data to perform automatic migrations.

We are almost done. The next step is to change the class files for the Event entity to reflect the new name. To do this we use my favorite feature of Xcode, the Refactor function. As implied by the name, this function will refactor and do a more advanced Find & Replace on your project for situations like these.

To do this you will need to open Event.m and select the text 'Event' on that file. Then go to: Edit > Refactor (Cmd+Shift+J). Up will pop a window with options and fields like the screenshot below. Enter in the word Lap in the 'Event to' field. Click preview and you will get the output of all the changes:

Lightweight Migrations

If you want to peer into what this will do then click on one of the files in the results panel and it will show you details of what it will change. It will also rename the Event.h and Event.m files for us. Just to note, previous xcdatamodel files with the particular NSManagedObject class being replaced will have a slight change. It will change the class name of the entity, not its entity name, so if a device has to go through migration it can still use the entities NSManagedObject class and related methods.

Hit apply. That's it. As a safety precaution it will also take a Snapshot, another great feature of XCode. So if it all goes wrong you can just go back to the previous snapshot, just like Time Machine but minus all the stars and swirly galaxy UI.

Unfortunately, that doesn't catch all of the changes needed. There are still some bits of the LapTimer application that reference Event. So a quick Find & Replace is in order. Go to: Edit > Find in Project. With the new window enter Event as the find and Lap as the replace. Uncheck 'Ignore Case' as we only want to pick up on the strict capitalization of the find. Then change the dropdown where 'Contains' is selected and select 'Whole Words' instead. This will narrow the find down to exactly to what we need.

Find and Replace

Click find. Review the changes. If all has gone well, then perform the replace!

Changing the active schema

So we have created a new version of the schema, made some modifications, and now it's time to get the application into the new schema.

Nothing tricky here. Select the new schema you want to set as active and navigate to: Design > Data Model > Set Current Version. The green tick will move to the new schema and it's all ready to go.

Build and run the application. All should be well and the application should open, migrate, and run. You won't notice this migration at all, not even a debugger output. But the fact that the code is running and working is proof that it has migrated.

Recap

So the application now has a modified entity name. This can happen time to time in the development of any application. You can add new properties with this lightweight migration method with this same process.

If you need to change the type of a property or do more advanced changes, then you will need to dive into Mapping Model Object migration. This is a more advanced method of what we performed, requiring extra configurations and code. The Apple iOS documentation has a really good run through of this advanced migration process.

Advertisement