Managed Object Model Migration - Learning Core Data for iOS (2014)

Learning Core Data for iOS (2014)

3. Managed Object Model Migration

Anyone who has never made a mistake has never tried anything new.

Albert Einstein

In Chapter 2, “Managed Object Model Basics,” the fundamentals of managed object models were introduced, yet you were constrained to just one entity and a few attributes. The next logical step is to add more to the model; however, this requires a number of preliminary steps in order to prevent crashes caused by these changes. This chapter will show how to add model versions and model mappings, as well as demonstrate different migration techniques you can choose when upgrading a model.

Changing a Managed Object Model

As an application evolves, its managed object model will probably need to change, too. Simple changes, such as attribute defaults, validation rules, and fetch request templates can be modified without consequence. Other more structural changes require that persistent stores be migrated to new model versions. If a persistent store doesn’t have the appropriate mappings and settings required to migrate data, the application will crash.


Note

To continue building the sample application, you’ll need to have added the previous chapter’s code to Grocery Dude. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LearningCoreData/GroceryDude-AfterChapter02.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to click Product > Clean. This practice ensures there’s no residual cache from previous projects using the same name. It is recommended that you use the iOS Simulator when following this chapter so you can inspect the contents of the SQLite database files easily.


Update Grocery Dude as follows to generate a model incompatibility error:

1. Run Grocery Dude once to ensure the existing model has been used to create the persistent store. You should see the words “Successfully added store” in the console log.

2. Select Model.xcdatamodeld in Xcode.

3. Add a new entity called Measurement.

4. Select the Measurement entity and add an attribute called abc. Set its type to String.

5. Re-run the application and examine the console log. You should now have generated arguably one of the most common Core Data errors, as shown in Figure 3.1. If this error has not appeared, delete the application and then click Product > Clean and retry from Step 1.

Image

Figure 3.1 Model changes make persistent stores incompatible.

This crash isn’t an issue when an application is in its initial development phase. To get past it, you can just delete the application and run it again. When the application is run for the first time after being deleted, the persistent store will be created based on the latest model. This will make the store compatible with the model, so the application won’t crash anymore. However, it won’t have any old data in it. As such, this scenario is unacceptable for any application already available on the App Store. There are a few approaches to migrating existing persistent stores, and the migration path you choose will be driven by the complexity of the changes and whether or not you’re using iCloud. Whatever you do, you’ll first need to become familiar with model versioning.

Update Grocery Dude as follows to revert to the original model:

1. Select Model.xcdatamodeld.

2. Delete the Measurement entity.

3. Re-run the application, which now should not crash.

Adding a Model Version

To avoid the crash previously demonstrated in Figure 3.1, you’ll need to create a new model version before making changes to the model. Ongoing, you should not remove old versions of a model. Old model versions are needed to help migrate older persistent stores to the current model version. If there are no existing persistent stores on customer devices, you can ignore model versioning until your application is on the App Store.

Update Grocery Dude as follows to add a model version:

1. Select Model.xcdatamodeld.

2. Click Editor > Add Model Version....

3. Click Finish to accept Model 2 as the version name.

You should now have two model versions, as shown in Figure 3.2.

Image

Figure 3.2 Multiple model versions

The new model Model 2.xcdatamodel will start out as a replica of Model.xcdatamodel. This makes it easy to modify the wrong version unintentionally. Before you edit a model, you should triple-check you have selected the correct one. You may wish to get into the habit of taking a snapshot, or even backing up the whole project prior to editing a model.

Update Grocery Dude as follows to reintroduce the Measurement entity:

1. Optionally take a snapshot or back up the Grocery Dude project.

2. Select Model 2.xcdatamodel.

3. Create a new entity called Measurement.

4. Select the Measurement entity, create an attribute called abc, and then set its type to String.

After you’ve added the new model version, you still need to set it as the current version before it will be used by the application.

Update Grocery Dude as follows to change the current model version:

1. Select Model.xcdatamodeld.

2. Click View > Utilities > Show File Inspector (or press Option+image+1).

3. Set the Current Model Version to Model 2, as shown in Figure 3.3.

Image

Figure 3.3 Setting the current model

Before you can successfully launch the application, you’ll need to configure migration options to tell Core Data how to migrate. Feel free to launch it again to generate the “Store is incompatible” error in the meantime.

Lightweight Migration

Whenever a new model is set as the current version, existing persistent stores must be migrated to use them. This is because the persistent store coordinator will try to use the latest model to open the existing store, which will fail if the store was created using a previous version of the model. The process of store migration can be handled automatically by passing the following options to a persistent store coordinator in an NSDictionary as a store is added to it:

image When NSMigratePersistentStoresAutomaticallyOption is YES and passed to a persistent store coordinator, Core Data will automatically attempt to migrate lower versioned (and hence incompatible) persistent stores to the latest model.

image When NSInferMappingModelAutomaticallyOption is YES and passed to a persistent store coordinator, Core Data will automatically attempt to infer a best guess at what attributes from the source model entities should end up as attributes in the destination model entities.

Using those persistent store coordinator options together is called lightweight migration and is demonstrated in bold in Listing 3.1. These options are set in an updated loadStore method of CoreDataHelper.m. Note that if you’re using iCloud, this is your only choice for migration.

Listing 3.1 CoreDataHelper.m: loadStore


if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
if (_store) {return;} // Don't load store if it's already loaded
NSDictionary *options =
@{
NSMigratePersistentStoresAutomaticallyOption:@YES
,NSInferMappingModelAutomaticallyOption:@YES
,NSSQLitePragmasOption: @{@"journal_mode": @"DELETE"}
};
NSError *error = nil;
_store = [_coordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:[self storeURL]
options:options error:&error];
if (!_store) {NSLog(@"Failed to add store. Error: %@", error);abort();}
else {NSLog(@"Successfully added store: %@", _store);}


Update Grocery Dude as follows to enable lightweight migration:

1. Replace all existing code in the loadStore method of CoreDataHelper.m with the code from Listing 3.1. For reference, the bold code shows the new changes.

2. Re-run the application, which should not crash.

From now on, any time you set a new model as the current version and lightweight migration is enabled, the migration should occur seamlessly.

Before other migration types can be demonstrated, some test data needs to be generated. Listing 3.2 contains code that generates managed objects based on the Measurement entity.

Listing 3.2 AppDelegate.m: demo (Inserting Test Measurement Data)


- (void)demo {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
for (int i = 1; i < 50000; i++) {
Measurement *newMeasurement =
[NSEntityDescription insertNewObjectForEntityForName:@"Measurement"
inManagedObjectContext:_coreDataHelper.context];

newMeasurement.abc =
[NSString stringWithFormat:@"-->> LOTS OF TEST DATA x%i",i];
NSLog(@"Inserted %@",newMeasurement.abc);
}
[_coreDataHelper saveContext];
}


Update Grocery Dude as follows to generate test data:

1. Create an NSManagedObject subclass of the Measurement entity. As discussed in Chapter 2, this is achieved by first selecting the entity and then clicking Editor > Create NSManagedObject Subclass... and following the prompts. When it comes time to save the class file, don’t forget to tick the Grocery Dude target.

2. Add #import "Measurement.h" to the top of AppDelegate.m.

3. Replace the demo method in AppDelegate.m with the code from Listing 3.2.

4. Run the application once. This will insert quite a lot of test data into the persistent store, which you can monitor by examining the console log. This may take a little while, depending on the speed of your machine. Please be patient as these objects are inserted. It’s important to have a fair amount of data in the persistent store to demonstrate the speed of migrations later.

The test data should now be in the persistent store, so there’s no need to reinsert it each time the application is launched. Note that the Items table view will still remain blank because it has not yet been configured to display anything.

The next step is to reconfigure the demo method to show some of what’s in the persistent store. The code shown in Listing 3.3 fetches a small sample of Measurement data. Notice that a new option is included that limits fetched results to 50. This is great for limiting how many results are fetched from large data sets, and even more powerful when mixed with sorting to generate a Top-50, for example.

Listing 3.3 AppDelegate.m: demo (Fetching Test Measurement Data)


- (void)demo {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
NSFetchRequest *request =
[NSFetchRequest fetchRequestWithEntityName:@"Measurement"];
[request setFetchLimit:50];
NSError *error = nil;
NSArray *fetchedObjects =
[_coreDataHelper.context executeFetchRequest:request error:&error];

if (error) {NSLog(@"%@", error);}
else {
for (Measurement *measurement in fetchedObjects) {
NSLog(@"Fetched Object = %@", measurement.abc);
}
}
}


Update Grocery Dude as follows to prevent duplicate test data from being inserted:

1. Replace the demo method in AppDelegate.m with the code from Listing 3.3.

2. Run the application.

3. Examine the contents of the Grocery-Dude.sqlite file using SQLite Database Browser, as explained previously in Chapter 2. Figure 3.4 shows the expected results.

Image

Figure 3.4 Test data ready for the next parts of this chapter

Ensure you close SQLite Database Browser before continuing.

Default Migration

Sometimes, you need more control than what lightweight migration offers. Let’s say, for instance, you want to replace the Measurement entity with another entity called Amount. In addition, you want the abc attribute from the Measurement entity to end up as an xyz attribute in theAmount entity. Any existing abc data should also be migrated to the xyz attribute. To achieve these requirements, you’ll need to create a model mapping to manually specify what maps to where. When the persistent store option NSInferMappingModelAutomaticallyOption is set to YES, Core Data still checks to see if there are any model-mapping files it should use before trying to infer automatically. It is recommended that you disable this setting while you’re testing a mapping model. This way, you can be certain that the mapping model is being used and is functioning correctly.

Update Grocery Dude as follows to disable automatic model mapping:

1. Set the NSInferMappingModelAutomaticallyOption option in the loadStore method of CoreDataHelper.m to @NO.

Update Grocery Dude as follows to add a new model in preparation for the migration from the Measurement entity to the Amount entity:

1. Optionally take a snapshot or back up the project.

2. Add a new model version called Model 3 based on Model 2.

3. Select Model 3.xcdatamodel.

4. Delete the Measurement entity.

5. Add a new entity called Amount with a String attribute called xyz.

6. Create an NSManagedObject subclass of the Amount entity. When it comes time to save the class file, don’t forget to tick the Grocery Dude target.

7. Set Model 3 as the current model version.

8. Run the application, which should crash with the error shown in Figure 3.5.

Image

Figure 3.5 A mapping model is required when mapping is not inferred.

To resolve the error shown in Figure 3.5, you need to create a mapping model that shows what fields map where. Specifically, the requirement is to map the old Measurement abc attribute to the new Amount xyz attribute.

Update Grocery Dude as follows to add a new mapping model:

1. Ensure the Data Model group is selected.

2. Click File > New > File....

3. Select iOS > Core Data > Mapping Model and then click Next.

4. Select Model 2.xcdatamodel as the Source Data Model and then click Next.

5. Select Model 3.xcdatamodel as the Target Data Model and then click Next.

6. Set the mapping model name to save as Model2toModel3.

7. Ensure the Grocery Dude target is ticked and then click Create.

8. Select Model2toModel3.xcmappingmodel.

You should now be presented with the model-mapping editor, as shown in Figure 3.6.

Image

Figure 3.6 The model-mapping editor

The mappings you’re presented with will be a best guess based on what Core Data can infer on its own. On the left you should see Entity Mappings, showing what source entities map to what destination entities. You should also see as in Figure 3.6 how the source Item entity has already inferred that it should map to the destination Item entity, which is a fair assumption. The naming standard of an entity mapping is SourceToDestination. With this in mind, notice the Amount entity doesn’t seem to have a source entity because it never existed in the source model.

Update Grocery Dude as follows to map the old Measurement entity to the new Amount entity:

1. Ensure Model2toModel3.xcmappingmodel is selected.

2. Select the Amount entity mapping.

3. Click View > Utilities > Show Mapping Model Inspector (if that’s not visible in the menu system, press Option+image+3). You need to be able to see the pane shown in Figure 3.7.

Image

Figure 3.7 Custom entity mapping of measurement to amount

4. Set the Source of Amount entity mapping to Measurement. The expected result is shown in Figure 3.7.

Because Measurement was selected as the source entity for the Amount destination entity, the Entity Mapping Name was automatically renamed to MeasurementToAmount. In addition, the mapping type changed from Add to Transform. For more complex implementations, you can specify a custom policy in the form of an NSEntityMigrationPolicy subclass. By overriding createDestinationInstancesForSourceInstance in the subclass, you can manipulate the data that’s migrated. For example, you could intercept the values of the abc attribute, set them all to title case, and then migrate them to the xyz attribute.

The Source Fetch option shown at the bottom of Figure 3.7 allows you to limit the migrated data to the results of a predicated (filtered) fetch. This is useful if you only want a subset of the existing data to be migrated. The predicate format you use here is the same as the format you would use when normally configuring a predicate, except you use $source variables. An example of a predicate that would filter out nil source data from the abc attribute is $source.abc != nil.

Select the ItemToItem entity mapping shown previously in Figure 3.6 and examine its attribute mappings. Notice how each destination attribute has a Value Expression set. Now examine the MeasurementToAmount entity mapping. Notice there’s no value expression for the xyzdestination attribute. This means that the xyz attribute has no source attribute, and you’ll need to set one using the same format used in the ItemToItem entity mapping. The original requirement was to map the abc attribute to the xyz attribute, so that’s what needs configuring here.

Update Grocery Dude as follows to set an appropriate value expression for the xyz destination attribute:

1. Set the Value Expression for the xyz destination attribute of the MeasurementToAmount entity mapping to $source.abc.

The mapping model is now ready to go; however, the demo method still fetches from the Measurement entity, which doesn’t exist under the new model.

Update Grocery Dude as follows to refer to the Amount entity instead of the Measurement entity:

1. Replace #import "Measurement.h" with #import "Amount.h" at the top of AppDelegate.m.

2. Replace the code in the demo method of AppDelegate.m with the code from Listing 3.4. Similar to the code being replaced, this code simply fetches a small sample of Amount data instead of Measurement data.

3. Run the application. The loading screen might display longer than usual due to the migration, depending on the speed of your machine.

Listing 3.4 AppDelegate.m: demo (Fetching Test Amount Data)


NSFetchRequest *request =
[NSFetchRequest fetchRequestWithEntityName:@"Amount"];
[request setFetchLimit:50];
NSError *error = nil;
NSArray *fetchedObjects =
[_coreDataHelper.context executeFetchRequest:request error:&error];

if (error) {NSLog(@"%@", error);}
else {
for (Amount *amount in fetchedObjects) {
NSLog(@"Fetched Object = %@", amount.xyz);
}
}


So long as the migration has been successful, the application won’t crash and you should see the expected result in the console log, as shown in Figure 3.8.

Image

Figure 3.8 Results of a successfully mapped model

To verify the migration has persisted to the store, examine the contents of the Grocery-Dude.sqlite file using the techniques discussed in Chapter 2. The expected result is shown in Figure 3.9, which illustrates the new ZAMOUNT table (that is, Amount entity) with the data from the old Measurement entity.

Image

Figure 3.9 A successfully mapped model

Make sure you close the SQLite Database Browser before continuing.

Migration Manager

Instead of letting a persistent store coordinator perform store migrations, you may wish to use a migration manager. Using a migration manager gives you total control over the files created during a migration and thus the flexibility to handle each aspect of a migration in your own way. One example of a benefit of using a migration manager is that you can report on the progress of a migration, which is useful for keeping the user informed (and less cranky) about a slow launch. Although most migrations should be quite fast, some large databases requiring complex changes can take a while to migrate. To keep the user interface responsive, the migration must be performed on a background thread. The user interface has to be responsive in order to provide updates to the user. The challenge is to prevent the user from attempting to use the application during the migration. This is because the data won’t be ready yet, so you don’t want the user staring at a blank screen wondering what’s going on. This is where a migration progress View Controller comes into play.

Update Grocery Dude as follows to configure a migration View Controller:

1. Select Main.storyboard.

2. Drag a new View Controller onto the storyboard, placing it above the existing Navigation Controller.

3. Drag a new Label and Progress View onto the new View Controller.

4. Position the Progress View in the center of the View Controller and then position the Label above it.

5. Widen the Label and Progress View to the width of the View Controller margins, as shown on the left in Figure 3.10.

Image

Figure 3.10 Migration View Controller

6. Configure the Label with Centered text that reads Migration Progress 0%, as shown on the left in Figure 3.10.

7. Configure the Progress View progress to 0.

8. Set the Storyboard ID of the View Controller to migration using Identity Inspector (Option+image+3) while the View Controller is selected.

9. Click Editor > Resolve Auto Layout Issues > Reset to Suggested Constraints in View Controller. Figure 3.10 shows the expected result.

The new migration View Controller has UILabel and UIProgressView interface elements that will need updating during a migration. This means a way to refer to these interface elements in code is required. A new UIViewController subclass called MigrationVC will be created for this purpose.

Update Grocery Dude as follows to add a MigrationVC class in a new group:

1. Right-click the existing Grocery Dude group and then select New Group.

2. Set the new group name to Grocery Dude View Controllers.

3. Select the Grocery Dude View Controllers group.

4. Click File > New > File....

5. Create a new iOS > Cocoa Touch > Objective-C class and then click Next.

6. Set Subclass of to UIViewController and Class name to MigrationVC and then click Next.

7. Ensure the Grocery Dude target is ticked and then create the class in the Grocery Dude project directory.

8. Select Main.storyboard.

9. Set the Custom Class of the new migration View Controller to MigrationVC using Identity Inspector (Option+image+3) while the View Controller is selected. This is in the same place as where the Storyboard ID was set.

10. Show the Assistant Editor by clicking View > Assistant Editor > Show Assistant Editor (or pressing Option+image+Return).

11. Ensure the Assistant Editor is automatically showing MigrationVC.h. The top-right of Figure 3.11 shows what this looks like. If you need to, just click Manual or Automatic while the migration View Controller is selected and select MigrationVC.h.

Image

Figure 3.11 Creating storyboard-linked properties to MigrationVC.h

12. Hold down Control while dragging a line from the migration progress label to the code in MigrationVC.h before @end. When you let go of the left mouse button, a pop-up will appear. In the pop-up, set the Name of the new UILabel property to label and ensure the Storage is set toStrong before clicking Connect. Figure 3.11 shows the intended configuration.

13. Repeat the technique in step 12 to create a linked UIProgressView property from the progress view called progressView.

To report migration progress, a pointer to the migration View Controller is required in CoreDataHelper.h.

Update Grocery Dude as follows to add a new property:

1. Show the Standard Editor by clicking View > Standard Editor > Show Standard Editor (or pressing image+Return).

2. Add #import "MigrationVC.h" to the top of CoreDataHelper.h.

3. Add @property (nonatomic, retain) MigrationVC *migrationVC; to CoreDataHelper.h beneath the existing properties.

To handle migrations manually, you’ll need to work out whether a migration is necessary each time the application is launched. To make this determination, you’ll need to know the URL of the store you’re checking to see that it actually exists. Providing that it does, you then compare the store’s model metadata to the new model. The result of this model comparison is used to determine whether the new model is compatible with the existing store. If it’s not, migration is required. The isMigrationNecessaryForStore method shown in Listing 3.5 demonstrates how these checks translate into code.

Listing 3.5 CoreDataHelper.m: isMigrationNecessaryForStore


#pragma mark - MIGRATION MANAGER
- (BOOL)isMigrationNecessaryForStore:(NSURL*)storeUrl {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
if (![[NSFileManager defaultManager] fileExistsAtPath:[self storeURL].path]) {
if (debug==1) {NSLog(@"SKIPPED MIGRATION: Source database missing.");}
return NO;
}
NSError *error = nil;
NSDictionary *sourceMetadata =
[NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
URL:storeUrl error:&error];
NSManagedObjectModel *destinationModel = _coordinator.managedObjectModel;
if ([destinationModel isConfiguration:nil
compatibleWithStoreMetadata:sourceMetadata]) {
if (debug==1) {
NSLog(@"SKIPPED MIGRATION: Source is already compatible");}
return NO;
}
return YES;
}


Update Grocery Dude as follows to implement a new MIGRATION MANAGER section:

1. Add the code from Listing 3.5 to the bottom of CoreDataHelper.m before @end.

Provided migration is necessary, the next step is to perform migration. Migration is a three-step process, as shown by the comments in Listing 3.6.

Listing 3.6 CoreDataHelper.m: migrateStore


- (BOOL)migrateStore:(NSURL*)sourceStore {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
BOOL success = NO;
NSError *error = nil;

// STEP 1 - Gather the Source, Destination and Mapping Model
NSDictionary *sourceMetadata = [NSPersistentStoreCoordinator
metadataForPersistentStoreOfType:NSSQLiteStoreType
URL:sourceStore
error:&error];

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

NSManagedObjectModel *destinModel = _model;

NSMappingModel *mappingModel =
[NSMappingModel mappingModelFromBundles:nil
forSourceModel:sourceModel
destinationModel:destinModel];

// STEP 2 - Perform migration, assuming the mapping model isn't null
if (mappingModel) {
NSError *error = nil;
NSMigrationManager *migrationManager =
[[NSMigrationManager alloc] initWithSourceModel:sourceModel
destinationModel:destinModel];
[migrationManager addObserver:self
forKeyPath:@"migrationProgress"
options:NSKeyValueObservingOptionNew
context:NULL];

NSURL *destinStore =
[[self applicationStoresDirectory]
URLByAppendingPathComponent:@"Temp.sqlite"];

success =
[migrationManager migrateStoreFromURL:sourceStore
type:NSSQLiteStoreType options:nil
withMappingModel:mappingModel
toDestinationURL:destinStore
destinationType:NSSQLiteStoreType
destinationOptions:nil
error:&error];
if (success) {
// STEP 3 - Replace the old store with the new migrated store
if ([self replaceStore:sourceStore withStore:destinStore]) {
if (debug==1) {
NSLog(@"SUCCESSFULLY MIGRATED %@ to the Current Model",
sourceStore.path);}
[migrationManager removeObserver:self
forKeyPath:@"migrationProgress"];
}
}
else {
if (debug==1) {NSLog(@"FAILED MIGRATION: %@",error);}
}
}
else {
if (debug==1) {NSLog(@"FAILED MIGRATION: Mapping Model is null");}
}
return YES; // indicates migration has finished, regardless of outcome
}


STEP 1 involves gathering the things you need to perform a migration, which are as follows:

image A source model, which you get from the metadata of a persistent store through its coordinator via metadataForPersistentStoreOfType

image A destination model, which is just the existing _model instance variable

image A mapping model, which is determined automatically by passing nil as the bundle along with the source and destination models

STEP 2 is the process of the actual migration. An instance of NSMigrationManager is created using the source and destination models. Before migrateStoreFromURL is called, a destination store is set. This destination store is just a temporary store that’s only used for migration purposes.

STEP 3 is only triggered when a migration has succeeded. The replaceStore method is used to clean up after a successful migration. When migration occurs, a new store is created at the destination; yet, this is no good to Core Data until the migrated store has the same location and filename as the old store. In order to use the newly migrated store, the old store is deleted and the new store is put in its place. In your own projects you may wish to copy the old store to a backup location first. The option to keep a store backup is up to you and would require slightly modified code in the replaceStore method. If you do decide to back up the old store, be aware that you’ll double your application’s storage requirements in the process.

The migration process is made visible to the user by an observeValueForKeyPath method that is called whenever the migration progress changes. This method is responsible for updating the migration progress View Controller whenever it sees a change to the migrationProgress property of the migration manager.

The code involved in the observeValueForKeyPath and replaceStore methods is shown in Listing 3.7.

Listing 3.7 CoreDataHelper.m: observeValueForKeyPath and replaceStore


- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {

if ([keyPath isEqualToString:@"migrationProgress"]) {

dispatch_async(dispatch_get_main_queue(), ^{

float progress =
[[change objectForKey:NSKeyValueChangeNewKey] floatValue];
self.migrationVC.progressView.progress = progress;
int percentage = progress * 100;
NSString *string =
[NSString stringWithFormat:@"Migration Progress: %i%%",
percentage];
NSLog(@"%@",string);
self.migrationVC.label.text = string;
});
}
}
- (BOOL)replaceStore:(NSURL*)old withStore:(NSURL*)new {

BOOL success = NO;
NSError *Error = nil;
if ([[NSFileManager defaultManager]
removeItemAtURL:old error:&Error]) {

Error = nil;
if ([[NSFileManager defaultManager]
moveItemAtURL:new toURL:old error:&Error]) {
success = YES;
}
else {
if (debug==1) {NSLog(@"FAILED to re-home new store %@", Error);}
}
}
else {
if (debug==1) {
NSLog(@"FAILED to remove old store %@: Error:%@", old, Error);
}
}
return success;
}


Update Grocery Dude as follows to continue implementing the MIGRATION MANAGER section:

1. Add the code from Listing 3.7 and then Listing 3.6 to the MIGRATION MANAGER section at the bottom of CoreDataHelper.m before @end.

To start a migration in the background using a migration manager, the method shown in Listing 3.8 is needed.

Listing 3.8 CoreDataHelper.m: performBackgroundManagedMigrationForStore


- (void)performBackgroundManagedMigrationForStore:(NSURL*)storeURL {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}

// Show migration progress view preventing the user from using the app
UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
self.migrationVC =
[sb instantiateViewControllerWithIdentifier:@"migration"];
UIApplication *sa = [UIApplication sharedApplication];
UINavigationController *nc =
(UINavigationController*)sa.keyWindow.rootViewController;
[nc presentViewController:self.migrationVC animated:NO completion:nil];

// Perform migration in the background, so it doesn't freeze the UI.
// This way progress can be shown to the user
dispatch_async(
dispatch_get_global_queue(
DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
BOOL done = [self migrateStore:storeURL];
if(done) {
// When migration finishes, add the newly migrated store
dispatch_async(dispatch_get_main_queue(), ^{
NSError *error = nil;
_store =
[_coordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:[self storeURL]
options:nil
error:&error];
if (!_store) {
NSLog(@"Failed to add a migrated store. Error: %@",
error);abort();}
else {
NSLog(@"Successfully added a migrated store: %@",
_store);}
[self.migrationVC dismissViewControllerAnimated:NO
completion:nil];
self.migrationVC = nil;
});
}
});
}


The performBackgroundManagedMigrationForStore method uses a storyboard identifier to instantiate and present the migration view. Once the view is blocking user interaction, the migration can begin. The migrateStore method is called on a background thread. Once migration is complete, the coordinator then adds the store as usual, the migration view is dismissed, and normal use of the application can resume.

Update Grocery Dude as follows to continue implementing the MIGRATION MANAGER section:

1. Add the code from Listing 3.8 to the MIGRATION MANAGER section at the bottom of CoreDataHelper.m before @end.

The best time to check whether migration is necessary is just before a store is added to a coordinator. To orchestrate this, the loadStore method of CoreDataHelper.m needs to be updated. If a migration is necessary, it will be triggered here. Listing 3.9 shows the code involved.

Listing 3.9 CoreDataHelper.m: loadStore


if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
if (_store) {return;} // Don't load store if it's already loaded

BOOL useMigrationManager = YES;
if (useMigrationManager &&
[self isMigrationNecessaryForStore:[self storeURL]]) {
[self performBackgroundManagedMigrationForStore:[self storeURL]];
} else {
NSDictionary *options =
@{
NSMigratePersistentStoresAutomaticallyOption:@YES
,NSInferMappingModelAutomaticallyOption:@NO
,NSSQLitePragmasOption: @{@"journal_mode": @"DELETE"}
};
NSError *error = nil;
_store = [_coordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:[self storeURL]
options:options
error:&error];
if (!_store) {
NSLog(@"Failed to add store. Error: %@", error);abort();
}
else {NSLog(@"Successfully added store: %@", _store);}
}


Update Grocery Dude as follows to finalize the Migration Manager:

1. Replace all existing code in the loadStore method of CoreDataHelper.m with the code from Listing 3.9.

2. Add a model version called Model 4 based on Model 3.

3. Select Model 4.xcdatamodel.

4. Delete the Amount entity.

5. Add a new entity called Unit with a String attribute called name.

6. Create an NSManagedObject subclass of the Unit entity. When it comes time to save the class file, don’t forget to tick the Grocery Dude target.

7. Set Model 4 as the current model.

8. Create a new mapping model with Model 3 as the source and Model 4 as the target. When it comes time to save the mapping model file, don’t forget to tick the Grocery Dude target and save the mapping model as Model3toModel4.

9. Select Model3toModel4.xcmappingmodel.

10. Select the Unit entity mapping.

11. Set the Source of the Unit entity to Amount and the Value Expression of the name destination attribute to $source.xyz. You should see the Unit entity mapping automatically renamed to AmountToUnit, as shown in Figure 3.12.

Image

Figure 3.12 Mapping model for AmountToUnit

You’re almost ready to perform a migration; however, the fetch request in the demo method still refers to the old Amount entity.

Update Grocery Dude as follows to refer to the Unit entity instead of the Amount entity:

1. Replace #import "Amount.h" with #import "Unit.h" at the top of AppDelegate.m.

2. Replace the code in the demo method of AppDelegate.m with the code shown in Listing 3.10. This code just fetches 50 Unit objects from the persistent store.

Listing 3.10 AppDelegate.m: demo (Fetching Test Unit Data)


NSFetchRequest *request =
[NSFetchRequest fetchRequestWithEntityName:@"Unit"];
[request setFetchLimit:50];
NSError *error = nil;
NSArray *fetchedObjects =
[_coreDataHelper.context executeFetchRequest:request error:&error];

if (error) {NSLog(@"%@", error);}
else {
for (Unit *unit in fetchedObjects) {
NSLog(@"Fetched Object = %@", unit.name);
}
}


The migration manager is finally ready! Run the application and pay close attention! You should see the migration manager flash before your eyes, alerting you to the progress of the migration. The progress will also be shown in the console log.

Image

Figure 3.13 Visible migration progress

Examine the contents of the ZUNIT table in the Grocery-Dude.sqlite file using the techniques discussed in Chapter 2. The expected result is shown in Figure 3.14. If you notice a -wal file in the Stores directory and you’re sure that the default journaling mode is disabled, you might need to click Product > Clean and run the application again to examine the contents of the sqlite file.

Image

Figure 3.14 Successful use of the Migration Manager

If you’ve reproduced the results shown in Figure 3.14, give yourself a pat on the back because you’ve successfully implemented three types of model migration! The rest of the book will use lightweight migrations, so it needs to be re-enabled.

Update Grocery Dude as follows to re-enable lightweight migration:

1. Set the NSInferMappingModelAutomaticallyOption option in the loadStore method of CoreDataHelper.m to @YES.

2. Set useMigrationManager to NO in the loadStore method of CoreDataHelper.m.

3. Remove all code from the demo method of AppDelegate.m.

The old mapping models and NSManagedObject subclasses of entities that don’t exist anymore are no longer needed. Although you could remove them, leave them in the project for reference sake.

Summary

You’ve now experienced lightweight migration, default migration, and using a Migration Manager to display progress. You should now be able to make an informed decision when determining between migration options for your own applications. Don’t forget that the only migration option for iCloud-enabled Core Data applications is lightweight migration. Adding model versions should now be a familiar procedure because the model has changed several times already.

Exercises

Why not build on what you’ve learned by experimenting?

1. Set the current model version to Model 3 and run the application. It should not crash because the downgrade of data is inferred automatically. Note that this is only because NSInferMappingModelAutomaticallyOption has been re-enabled. In reality, you would need a Model4toModel3mapping model to map attributes properly.

2. Examine the contents of the ZAMOUNT table in Grocery-Dude.sqlite and you’ll notice something critical: Where has all the data gone? There was no mapping model, so all the ZUNIT data was lost during the downgrade!

3. Set the current model to Model 4 and re-enable the migration manager in the loadStore method of CoreDataHelper.m by setting useMigrationManager to YES.

4. Run the application to witness another manual migration, which will be extremely fast because there is no data in the store. Set useMigrationManager to NO before continuing to the next chapter.