Monday, June 23, 2014

Core Data Fears

Fears in general are base on rumors and a fundamental lack of understanding.  Hopefully by the end of this post all of our fears will be resolved and we can move forward with Core Data with rumors dispelled and a solid understanding of what we are doing.

Core Data is maybe on of the best reasons to do development on OSX or iOS it is an amazingly well done framework.  There is one very scary deterrent to it though; if you change the underlying data model incorrectly you can break your app.  The only way to fix it is to force users to uninstall and reinstall your app, which is a scenario you never want to find yourself in.

There is actually a large amount of very technical (and scary) documentation on how to do this correctly:
https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreDataVersioning/Articles/Introduction.html

You will definitely need to check out that document to go along with this post, but I'll try to boil it down to keep it simple for us.

Before we get started, I want to stress to you as you begin developing your app with core data, not to take the easy way out.  When you're developing your app and making model changes it is easy to uninstall and reinstall your app when you make data changes, don't do that!
Take the time now to learn the versioning process from the beginning, otherwise when you need to make the data change for an app in production you'll be too scared and unsure of yourself to do it properly.

At a high level this is what you need to do to make changes to your Core Data Model:

  1. Create a new version of your model (see the documentation)
  2. Mark the new model as as active

Once you have gone through and made the data model changes you need to do, you should have the information you need to decide if you can do a lightweight migration or if you will have to use a Migration Manager.

Unfortunately the documentation on when you can and can't use the lightweight migration is not very clear, however as a general rule if you are adding, removing or renaming things (entities or attributes) lightweight migration changes will "probably" work for you.  If you are completely re-working your data model then you will need to to use a migration manager, which may be covered in a future post.

OK, so having said all that let's talk a bit about a simple common scenario.  I am writing a super simple app that collects the user's name and stores it into an entity called TraxUser.   This is what that data model looks like:


Next we want to add another entity so that we can store some info about an iBeacon the user might run across:

So, now we have versioned our data model, made the changes.  Now if you were to be so naive as to run your app, you will get a pretty scary crash that looks like this:
2014-06-23 12:05:54.280 Trax[6122:60b] Unresolved error Error Domain=NSCocoaErrorDomain Code=134100 "The operation couldn’t be completed. (Cocoa error 134100.)" UserInfo=0x14d40530 {metadata={
    NSPersistenceFrameworkVersion = 479;
    NSStoreModelVersionHashes =     {
        TraxUser = <21f8bf1e 14685e4a="" 51457eb2="" 749ddd4d="" 83698e64="" a319d28f="" ceec364d="" db05b3aa="">;
    };
    NSStoreModelVersionHashesVersion = 3;
    NSStoreModelVersionIdentifiers =     (
        ""
    );
    NSStoreType = SQLite;
    NSStoreUUID = "78366830-BF50-4F5A-9142-893EE6C91619";
    "_NSAutoVacuumLevel" = 2;
}, reason=The model used to open the store is incompatible with the one used to create the store}, {
    metadata =     {
        NSPersistenceFrameworkVersion = 479;
        NSStoreModelVersionHashes =         {
            TraxUser = <21f8bf1e 14685e4a="" 51457eb2="" 749ddd4d="" 83698e64="" a319d28f="" ceec364d="" db05b3aa="">;
        };
        NSStoreModelVersionHashesVersion = 3;
        NSStoreModelVersionIdentifiers =         (
            ""
        );
        NSStoreType = SQLite;
        NSStoreUUID = "78366830-BF50-4F5A-9142-893EE6C91619";
        "_NSAutoVacuumLevel" = 2;
    };
    reason = "The model used to open the store is incompatible with the one used to create the store";
}

If/when you get this error it feels like the world has just ended and you go into panic mode big time.  Fear not! There is good news! You can go back to the previous version of your model and make it active and your app will start just again.  

For our change (adding a new entity) we can simply add this code into our app delegate under this method
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator  

by default this method has a large block of comments in it guiding you a bit on what needs to be done.  In all actuality the code change to make the migration happen is very simple.  

The default code that you are given should look like this:
// Returns the persistent store coordinator for the application.
// If the coordinator doesn't already exist, it is created and the application's store added to it.
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (_persistentStoreCoordinator != nil) {
        return _persistentStoreCoordinator;
    }
    
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Trax.sqlite"];
    
    NSError *error = nil;
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error])

All that needs to change for this to work is to add a new dictionary to this method:

    
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES],
                             NSMigratePersistentStoresAutomaticallyOption,
                             [NSNumber numberWithBool:YES],
                             NSInferMappingModelAutomaticallyOptionnil];
and change this line:
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error])
to use the new dictionary that was just created:
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error])

Adding this dictionary in to the options you are telling the core data framework to migrate the data automatically and to infer the changes without help.  Easy enough!

And that is all there is to it!  By versioning the changes you make to your data model and making those two small changes to your AppDelegate you can make small incremental changes to your map very easily.

Good Luck using and versioning Core Data.  Remember to start getting familiar with CoreData early in your development process so when the inevitable time comes to change your data you can do it with confidence.

Happy Coding!

-Aaron

No comments: