Core Data Migration Headache

I’m happy as long as CoreData is able to do a light weight migration. But from time to time the schema changes are to big and this is not possible. This happend to me yesterday. And the headache was terrible.

I’m currently working on version 2.0.0 of our lovely PlusPoints application which is used by students all over the world to track their grades in school. As you can imagine the model changes in big parts as we are introducing many new features. There are easy things like adding a new entity or renaming a property but also harder things I didn’t do before with CoreData. PlusPoints was one of our first applications and started back in 2010. So the old model is a bit messed up. For example I have dates stored as NSString (without any validation…).

I played for 2 days with CoreData’s mapping models to do this sort of migrations. I used this tutorial on how to do custom migrations. But when it came to renaming entities, properties and transforming the property type at the same time, the battle against Xcode began. I was not able to get all that things working together. And on the top of it, Xcode crashed about every 10-15 minutes. I needed something else.

So I decided to try a different approach of doing that migration. The Idea is the following: I want a complete new model and just move the data from the old database into the new one. That will make the development of the new version much easier, because I can design the model from ground up without having to think about migrations and renaming. Short: Without the CoreData Headache.

But to get there the way was longer than I thought..

Preparing the old model

I had to rename the classes (not entities!) of the entities that will exist in the new model aswell. The reason for this is, that I can’t have two classes with the same name in one project. I just used the Refactor capability of Xcode to rename Semester to SemesterOld. I also had to change the Class name in the .xcdatamodeld file. The Semester Entity from the old model will use the class SemesterOld from now on.

Setting up the new model

The next step was to setup and design the new model. There’s nothing special about it. I just added another .xcdatamodeld file to the project and created the new model

Migration

The trickiest part was the migration of the data from the old model to the new model. For this task I wrote a small class called Updater. You can find it in this gist.

The idea behind it is easy. When the update method is called 3 things are done:

  • The ‘old’ CoreData stack is created
  • The ‘new’ CoreData stack is created
  • All entities are (manually) transferred from the old stack to the new stack.

To fully understand the code you may have a look at MagicalRecord which makes this code much more readable.

There’s also one other little thing going on in update. If you look at the database paths, you will see that the old is in the Documents folder and the new is in the Library folder. The reason is that I want to clean the Documents folder to use it for iTunes Filesharing.

Setting up the CoreData Stack

It is important that the update method of the Updater class is run before the application does the CoreData setup (usually in the AppDelegate class). Otherwise the application will crash.

Once the update method is run, I had a new Database.sqlite file in the applications’ Library folder as expected. But when I tried to run the application it crashed with the error:

Can't merge models with two different entities named 'Semester'

Why does CoreData try to merge my different .xcdatamodeld files?

Because I told it to do so. My code to create the NSManagedObjectModel in the AppDelegate looks like this:

// Returns the managed object model for the application.
// If the model doesn't already exist, it is created from the application's model.
- (NSManagedObjectModel *)managedObjectModel
{
    if (_managedObjectModel != nil) {
        return _managedObjectModel;
    }
    _managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:nil];
    return _managedObjectModel;
}

Have a look at the line

_managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:nil];

This is what causes the crash. So I told CoreData which model to use with the following 2 lines:

NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Database2" withExtension:@"momd"];
_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];

CoreData doesn’t try to merge anything. It uses the new model I just created with the database that contains the migrated data. That’s exactely what I wanted!

Conclusion

I can now go on with the development of PlusPoints with a complete fresh model. I don’t have to worry about light weight migrations and renaming identifiers. The updater class does all that for me. I’m sure this will simplify the development and prevent the one and other NSInternalInconsistencyException.