Mixing Inferred and Custom Core Data migrations

Photo by MarcProudfoot, http://www.flickr.com/photos/progressionuk/8395619662/

Photo by MarcProudfoot, http://www.flickr.com/photos/progressionuk/8395619662/

Migrating between Core Data models is simple – until you need to make changes more complex than adding an entity or attribute.  A migration step like deriving data from two columns for a new attribute (such as combining firstName and lastName into normalizedName for faster searches) not only requires a custom migration policy, but prevents a simple invocation of lightweight migration between other models.

For example:

  • the migration from model 1 to 2 only adds a new attribute, and can therefore use lightweight migration
  • the migration from model 2 to 3 requires a custom migration class
  • then migrating from model 1 to 3 using a single call to the Core Data API is impossible without a custom migration class from 1 to 3

Writing (n – 1) migrations when creating your nth model is clearly untenable.

Instead, consider an iterative approach that combines lightweight and custom migrations.  In essence, you only define custom mapping models or migration classes between two models when necessary, and rely on inferred mapping models for all other migration steps.

I’ve written a small class called ALIterativeMigrator along with an example project to demonstrate the concept.

The ALIterativeMigrator API is as simple as defining an ordered list of model file names along which migration should occur, and calling a single method to migrate the persistent store prior to calling [NSPersistentStoreCoordinator addPersistentStoreWithType...]:

NSArray* modelNames =
   @[
      @"Model 1",
      @"Model 2",
      @"Model 3",
      @"Model 4"
   ];

if (![ALIterativeMigrator iterativeMigrateURL:storeURL
                                       ofType:NSSQLiteStoreType
                                      toModel:[self managedObjectModel]
                            orderedModelNames:modelNames
                                        error:&error])
{
   NSLog(@"Error migrating to latest model: %@", error);
}

The iterativeMigrateURL method can migrate from any model to any other model in either direction – from 1 to 4, or 4 to 1, or 2 to 3, etc.

The steps performed by the iterative migrator are relatively straightforward.

1. Check that the persistent store is not already at the destination model using the store metadata:

NSDictionary* sourceMetadata =
 [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:storeType
                                                            URL:url
                                                          error:error];
if ([finalModel isConfiguration:nil compatibleWithStoreMetadata:sourceMetadata])
{
   return YES;
}

2. Load the persistent store’s current model using the store metadata, which will be used to determine the list of relevant models for this migration:

NSManagedObjectModel* sourceModel =
 [NSManagedObjectModel mergedModelFromBundles:nil
                             forStoreMetadata:sourceMetadata];

3. Load all the named models passed in the orderedModelNames parameter. The iterative migrator assumes these files are stored at the top level of the main bundle or inside a *.momd directory. For each model name, it looks up the model file URL and loads the model object:

NSManagedObjectModel* model = 
 [[NSManagedObjectModel alloc] initWithContentsOfURL:modelUrl];

4. Narrow the list of models down to those between the source and destination by walking the list and checking whether each model is the source, the destination, or in between.  If the destination comes before the source in the list, reverse the list for a downward migration.

5. Find or create a mapping model for each pair of adjacent models in the list of relevant models.  A custom mapping model can be loaded from the bundle or an inferred mapping model can be created:

// Check whether a custom mapping model exists.
NSMappingModel* mappingModel = [NSMappingModel mappingModelFromBundles:nil
                                                        forSourceModel:modelA
                                                      destinationModel:modelB];

// If there is no custom mapping model, try to infer one.
if (nil == mappingModel)
{
   mappingModel = [NSMappingModel inferredMappingModelForSourceModel:modelA
                                                    destinationModel:modelB
                                                               error:error];
   if (nil == mappingModel)
   {
      return NO;
   }
}

6. Again for each pair of adjacent models, migrate the persistent store from the first model to the second model. In the iterative migrator, this is done using backup files in case the migration fails, but boils down to a pair of NSMigrationManager calls:

NSMigrationManager* migrator = [[NSMigrationManager alloc]
                               initWithSourceModel:sourceModel
                               destinationModel:targetModel];

if (![migrator migrateStoreFromURL:sourceStoreURL
                              type:sourceStoreType
                           options:nil
                  withMappingModel:mappingModel
                  toDestinationURL:tempDestinationStoreURL
                   destinationType:sourceStoreType
                destinationOptions:nil
                             error:error])
{
   return NO;
}

After the migration between the last pair of models, the persistent store has been updated to the destination model.  Thus the migrator class iteratively migrates the persistent store through all of the relevant models, minimizing the number of custom models that need to be written.

The efficiency of this algorithm could likely be improved by checking whether an inferred mapping model can be created between non-adjacent models, which could allow skipping migration steps.

Read more

Customizing Core Data Migrations” is a recent, useful blog post that helps explain the different kinds of migrations.

Noah Harrison

Noah Harrison

Senior software engineer at Art+Logic.
Noah Harrison

Latest posts by Noah Harrison (see all)

Tags:

Creative Commons License

This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.