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.