Best Practices For Database Migration in iOS Applications
Azoft Blog Best Practices For Database Migration in iOS Applications

Best Practices For Database Migration in iOS Applications

By July Nikolay on May 16, 2013

Data migration is something you'll probably need to implement sooner or later for any more or less popular application that is actively maintained and regularly updated.

Suppose we are working on an iOS application that stores a list of user's contacts. In the first version of the app, contact information was stored in the form of this structure: {NSString *fullName; NSUInteger identifier} (see left diagram in the picture). The first version was released early and immediately published in the AppStore to collect feedback and adjust the future development goals.

Soon the design changes and we no longer store identifier in an NSUInteger variable. Besides, the name of a contact is now represented as two strings (see right diagram in the picture). All three attributes are strings now.

The first version of the app: {NSString *fullName; NSUInteger identifier}

Of course, now we have to decide how to treat the existing users who are already storing some contacts in the installed app. Automatic migration is not going to help here, because it is unable to convert an NSNumber (number with NSUIntger) to string, not to mention the change to fullName field.

The first solution is to simply delete the old database and create a new one from scratch using the new model. The second solution, which the users will actually prefer, is to implement data migration to the new version. So let's talk about implementing migration in more detail here.

First we need to create a Mapping Model (New File > Core Data > Mapping Model). Choose the first version of your model as the source model and the second version as the destination.

Next, we need to subclass NSEntityMigrationPolicy and redefine the following method:

createDestinationInstancesForSourceInstance:entityMapping:manager:error:

For our example case the method is going to look like below:

- (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)sInstance entityMapping:(NSEntityMapping *)mapping manager:(NSMigrationManager *)manager error:(NSError **)error
{
  NSManagedObject *newObject = [NSEntityDescription insertNewObjectForEntityForName:[mapping destinationEntityName] inManagedObjectContext:[manager destinationContext]];
 
             NSString *fullName = [sInstance valueForKey:@"fullName"];
             NSArray *nameComponentsArray = [fullName componentsSeparatedByString:@" "];
NSString *lastName = [nameComponentsArray lastObject];
NSString *firstName = [nameComponentsArray[0];
 
  [newObject setValue:firstName forKey:@"firstName"];
             [newObject setValue:lastName forKey:@"lastName"];
 
  [manager associateSourceInstance:sInstance withDestinationInstance:newObject forEntityMapping:mapping];
 
 return YES;
}

There is no need to implement migration for every object by hand, only for those that fail to auto-migrate. The Mapping Model describes which classes should be processed by custom migration code, and which should be migrated automatically. If the automatic migration will not work, it is possible to change the migration policy later in the migration mapping editor.

In a really complex situation, like several different model versions with different migration logic, you can add version identifiers to the function above and specify as many migration algorithms as required.

The following method may be used to open a model:

+ (NSManagedObjectModel *)mergedModelFromBundles:(NSArray *)bundles forStoreMetadata:(NSDictionary *)metadata.

But it will work only if your project has a single data model. If you are updating more than one model, use the following method.

Let's assume the model we're working on is called "dataModel".

NSURL *dstStoreURL = [[NSBundle mainBundle] URLForResource:@"dataModel.momd" withExtension:nil];
 
 NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:dstStoreURL];

Let's create a coordinator to interact with the model database:

NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
 
 NSError *error = nil;

Next we need to copy the old database model with the new name and delete the original one:

NSFileManager * fileManager = [[NSFileManager alloc] init];
 
 NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask, YES);
 NSString *documentsDirectory = [paths objectAtIndex:0];
 NSString *documentDBFolderPath = [documentsDirectory stringByAppendingPathComponent:@"Application Support/PROJECT_NAME/dataModel.sqlite"];
 
 NSURL *storeFileURL = [NSURL fileURLWithPath:documentDBFolderPath];
 
 NSMutableString *url = [[NSMutableString alloc] initWithString:documentDBFolderPath];
 
 NSRange range = [url rangeOfString:storeFileName];
 
 if (range.location != NSNotFound) {
     [url replaceCharactersInRange:range withString:@"newDataModel.sqlite"];
 }
 
 NSURL *newStoreFileURL = [NSURL fileURLWithPath:url];
 [url release];
 
 [fileManager moveItemAtURL:storeFileURL toURL:newStoreFileURL error:&error ];
 
 error = nil;

To open the original version of the model (as there are more than one model in our project) we'll use the URL:

NSURL *storeURL = [dstStoreURL URLByAppendingPathComponent:@"dataModel.mom"];

NSManagedObjectModel *sourceModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:storeURL];

The perfect variant will be to open Mapping Model specifying the original and final models but we most probably can't, getting a nil model( mappingModel == nil). That's why we need to find the address of migration map in NSBundle.

Next we open the migration model:

NSURL *migrationModelUrl = [[NSBundle mainBundle] URLForResource:@"ModelMigration" withExtension:@"cdm"];
 NSMappingModel *mappingModel = [[NSMappingModel alloc] initWithContentsOfURL:migrationModelUrl];

Create migration manager:

NSMigrationManager *manager = [[NSMigrationManager alloc] initWithSourceModel:sourceModel destinationModel:model];

In some cases you might have an error message indicating that the existing database store doesn't match your model. This usually means, that the migration map wasn't properly created and you have to manually set the versionHashes for the source and destination versions of your model.

error = nil;

NSArray *newEntityMappings = [NSArray arrayWithArray:mappingModel.entityMappings];
 for (NSEntityMapping *entityMapping in newEntityMappings) {
     [entityMapping setSourceEntityVersionHash:[sourceModel.entityVersionHashesByName valueForKey:entityMapping.sourceEntityName]];
     [entityMapping setDestinationEntityVersionHash:[model.entityVersionHashesByName valueForKey:entityMapping.destinationEntityName]];
 
 }
 mappingModel.entityMappings = newEntityMappings;

Finally, the migration is executed by the following code:

BOOL result = [manager migrateStoreFromURL:newStoreFileURL type:NSSQLiteStoreType
                      options:nil withMappingModel:mappingModel toDestinationURL:storeFileURL
              destinationType:NSSQLiteStoreType destinationOptions:nil error:&error];

In storeFileUrl we have the location of a new database, which we can now open using the new model.

Apple developer documentation describes a less complicated way of doing all of the above: creating a migration map, specifying source and destination model, then simply using Migration Manager for the real work. But that will likely fail, even if you only need to migrate the same model from one version to another. Things will break because of the map and models mismatch. If you use several different models for storing different data in different files, then I advise you to try the above way of migrating your data.

VN:F [1.9.22_1171]
Rating: 3.1/5 (7 votes cast)
VN:F [1.9.22_1171]
Rating: +1 (from 1 vote)
Best Practices For Database Migration in iOS Applications, 3.1 out of 5 based on 7 ratings



Request a Free Quote
 
 
 

Please enter the result and submit the form

Content created by July Nikolay