Versioning and Migrating Data - Pro iOS Persistence: Using Core Data (2014)

Pro iOS Persistence: Using Core Data (2014)

Chapter 6. Versioning and Migrating Data

As you develop Core Data–based applications, you usually don’t get your data model exactly right the first time. You start by creating a data model that seems to meet your application’s data needs, but as you progress through the development of the application, you’ll often find that your data model needs to change to serve your growing vision of what your application should do. During this stage of your application’s life, changing your data model to match your new understanding of the application’s data poses little cost: your application will no longer launch, crashing on startup with the following message:

The model used to open the store is incompatible with the one used to create the store.

You resolve this issue either by finding the database file on the file system and deleting it or by deleting your fledgling application from the iPhone Simulator or your device. Either way, the database file that uses your outdated schema disappears, along with data in the persistent store, and your application re-creates the database file the next time it launches. You’ll probably do this several times during the development of your application.

Once you release your application, however, and people start using it, they will accumulate data they deem important in the persistent stores on their devices. Asking them to delete their data stores any time you want to release a new version of the application with a changed data model will drop your app instantly to one-star status, and the people commenting will decry Apple’s rating system for not allowing ratings of zero or even negative stars.

Does that mean releasing your application freezes its data model? That the data model in the 1.0 version of your application is permanent? That you’d better get the first public version of the data model perfect, because you’ll never be able to change it? Thankfully, no. Apple anticipated the need for improving Core Data models over the life of applications and built mechanisms for you to change data models and then migrate users’ data to the new models, all without Apple’s intervention or even awareness. This chapter goes through the process of versioning your data models and migrating data across those versions, however complex the changes you’ve made to the model.

Versioning

To take advantage of Core Data’s support for versioning your data model and migrating data from version to version, you start by explicitly creating a new version of the data model. To illustrate how this works, let’s bring back the BookStore application from Chapter 4. Make a copy of it, because you’ll be changing its data model several times in this chapter. In the original version of BookStore, you created a data model that makes the model appear as shown in Figure 6-1.

image

Figure 6-1. The single-version data model

BookStore.xcdatamodeld contains the model and is actually a directory on your file system. It contains the files necessary to create the Core Data storage when your application runs. Since there is only one version of the object model, it is automatically the current version. To add a new version, from the Xcode menu, select Editor image Add Model Version. A panel displays and allows you to enter a version name and select the model on which the new version is based, as shown in Figure 6-2.

image

Figure 6-2. Creating a new model version

Accept the defaults and select Finish. Xcode creates a new directory inside BookStore.xcdatamodeld called BookStore 2.xcdatamodel, as shown in Figure 6-3. Each listing below BookStore.xcdatamodeld represents a version of your data model; the green check mark denotes the version your application is currently using.

image

Figure 6-3. The data model with two versions

You can see that your original data model, BookStore.xcdatamodeld, is the current version. Before changing the current version from BookStore.xcdatamodeld to BookStore 2.xcdatamodel, run the BookStore application to create books so you have data to migrate across versions. You need data in your database to test that the data migrations throughout this chapter work properly.

Finally, we need to make a slight change to the BookStore application. In Chapter 4, we wanted to be able to have a clean data store for every run. Since we now want to test migrating existing data, we need to make sure the old data store isn’t wiped out when the application starts. OpenViewController.m or ViewController.swift and edit the viewDidAppear: method by commenting the following lines:

// [self initStore];// [self insertSomeData];

or

// self.initStore()
// self.insertSomeData()

Launch the application again to make sure nothing is created. It should only display the output of the showExampleData method.

Book fetched: The second bookTitle: The second book, price: 15.00Book fetched: The third bookTitle: The third book, price: 10.00Book fetched: The first bookTitle: The first book, price: 10.00Book fetched: The fourth bookTitle: The fourth book, price: 12.00

Now, suppose you want to add a new author attribute to the Book entity. If you edit your current model and add an author attribute to the Book entity, you will have the unpleasant surprise of seeing your application crash on startup. That’s because the data stored in the data store does not align with the new Core Data model. One option to alleviate this problem is to delete your existing data store. This is an acceptable option while you are developing your app, but if you already have customers using this app, this will trigger their wrath. For a smoother experience, we strongly recommend versioning your model and using Core Data’s migrations to preserve your users’ data by migrating it from the old model to the new. Any changes you make to your data model go into the new version of the model, not into any of the old ones. For you, this means that you’ll add the name attribute to the Book entity in the BookStore 2 data model.

To make this change, select the BookStore 2 data model, and then add an author attribute of type String to the Book entity in the normal way. Figure 6-4 shows the Xcode window with the new version of the Core Data model and the author attribute added to the Book entity.

image

Figure 6-4. The Xcode window with a second version of the model

To change the current version of the model from BookStore to BookStore 2, select BookStore.xcdatamodeld and set the current version in the Utility panel to BookStore 2, as shown in Figure 6-5.

image

Figure 6-5. Updating the current data model version to BookStore 2

At this point, your application has a new current version of the data model, but it’s not ready to run yet. You need to define a policy for migrating the data from the version of the model called BookStore to the one called BookStore 2. If you tried running the application now, you would get a crash because, just as if you had changed the first model, the data store does not match the current model. The application needs to be told how you want it to handle switching from the old model to the new one. The rest of this chapter discusses the various ways to do that.

Lightweight Migrations

Once you have a versioned data model, you can take advantage of Core Data’s support for migrations as you evolve your data model. Each time you create a new data model version, users’ application data must migrate from the old data model to the new. For the migration to occur, Core Data must have rules to follow to know how to properly migrate the data. You create these rules using a “mapping model.” Support for creating mapping models is built in to Xcode. For certain straightforward cases, however, Core Data has enough smarts to figure out the mapping rules on its own without requiring you to create a mapping model. Called lightweight migrations, these cases represent the least work for you as a developer. Core Data does all the work and migrates the data. This section details the lightweight migration process and walks you through the list of changes the process supports and how to implement them.

For your migration to qualify as a lightweight migration, your changes must be confined to this narrow band.

· Add or remove a property (attribute or relationship).

· Make a nonoptional property optional.

· Make an optional attribute nonoptional, as long as you provide a default value.

· Add or remove an entity.

· Rename a property.

· Rename an entity.

In addition to requiring less work from you, a lightweight migration using a SQLite data store runs faster and uses less space than other migrations. Because Core Data can issue SQL statements to perform these migrations, it doesn’t have to load all the data into memory in order to migrate them, and it doesn’t have to move the data from one store to the other. Core Data simply uses SQL statements to alter the SQLite database in place. If feasible, you should aggressively try to confine your data model changes to those that lightweight migrations support. If not, the other sections in this chapter walk you through more complicated migrations.

Migrating a Simple Change

In a previous section, “Versioning,” you created a new model version in the BookStore application called BookStore 2, and you added a new attribute called author to the Book entity in the BookStore 2 model. The final step to performing a lightweight migration with that change is to tell the persistent store coordinator two things.

· It should migrate the model automatically.

· It should infer the mapping model.

You do that by passing those instructions in the options parameter of the persistent store coordinator’s addPersistentStoreWithType: method. You first set up options, an dictionary, as follows:

NSDictionary *options = @{
NSMigratePersistentStoresAutomaticallyOption: @YES,
NSInferMappingModelAutomaticallyOption: @YES
}; // Objective-C version

let options = [NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true] // Swift version

This code creates a dictionary with two entries.

· One with the key of NSMigratePersistentStoresAutomaticallyOption, value of YES or true, which tells the persistent store coordinator to automatically migrate the data.

· One with the key of NSInferMappingModelAutomaticallyOption, value of YES or true, which tells the persistent store coordinator to infer the mapping model.

You then pass this options object, instead of nil, for the options parameter in the call to addPersistentStoreWithType:. Open the AppDelegate.m file or the AppDelegate.swift file and change the persistentStoreCoordinator method to look like Listing 6-1 (Objective-C), or change the code to initialize persistentStoreCoordinator inside the managedObjectContext closure to look like Listing 6-2 (Swift).

Listing 6-1. Creating the Persistent Store Coordinator with Migration Options (Objective-C)

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (_persistentStoreCoordinator != nil) {
return _persistentStoreCoordinator;
}

_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"BookStoreEnhanced.sqlite"];

NSDictionary *options = @{
NSMigratePersistentStoresAutomaticallyOption: @YES,
NSInferMappingModelAutomaticallyOption: @YES
};

NSError *error = nil;
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) {
[self showCoreDataError];
}

return _persistentStoreCoordinator;
}

Listing 6-2. Creating the Persistent Store Coordinator with Migration Options (Swift)

// Initialize the persistent store coordinator
let storeURL = AppDelegate.applicationDocumentsDirectory.URLByAppendingPathComponent("BookStoreEnhancedSwift.sqlite")
var error: NSError? = nil
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel!)

let options = [NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true]

if(persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: options, error: &error) == nil) {
self.showCoreDataError()
return nil
}

That’s all you have to do to migrate the data. Build and run the application, which no longer crashes but instead shows all the books you’ve created. Open a Terminal and navigate to the directory that contains the SQLite file for the BookStore application. Unlike early versions of iOS prior to iOS 5, which kept both the pre-migration and the post-migration versions of the database file, recent iOS versions keeps only the post-migration version. You’ll find the post-migration file, BookStoreEnhanced.sqlite. Open it using the sqlite3 application and run the .schemacommand to see the definition for the ZBOOK table. You can see that the BookStoreEnhanced.sqlite file has added a column for the name attribute: ZAUTHOR VARCHAR, as shown.

sqlite> .schema ZBOOKCREATE TABLE ZBOOK ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZCATEGORY INTEGER, ZPRICE FLOAT, ZAUTHOR VARCHAR, ZTITLE VARCHAR);CREATE INDEX ZBOOK_ZCATEGORY_INDEX ON ZBOOK (ZCATEGORY);

Renaming Entities and Properties

Lightweight migrations also support renaming entities and properties but require a little more effort from you. In addition to changing your model, you must specify the old name for the item whose name you changed. You can do this in one of two ways.

· In the Xcode data modeler

· In code

The Xcode data modeler is the simpler option. When you select an entity or property in the Xcode data modeler, general information about that entity or property shows in the Utilities panel to the right. With the Utilities panel displayed, open the Data Model inspector, and then the Versioning section. In the Renaming ID field, enter the old name of whatever you’ve changed (see Figure 6-6).

image

Figure 6-6. The Versioning section with the Renaming Identifier field

If you insist on specifying the old name in code, you’d make a call to the setRenamingIdentifier: method of NSEntityDescription or NSPropertyDescription, depending on what you’ve renamed, passing the old name for the entity or property. You do this after the model has loaded but before the call to open the persistent store (addPersistentStoreWithType:). If, for example, you want to change the name of the Book entity to Publication, you’d add the following code before the call to addPersistentStoreWithType:

// Objective-C
NSEntityDescription *publication = [[managedObjectModel entitiesByName] objectForKey:@"Publication"];
[publication setRenamingIdentifier:@"Book"];

// Swift
if let publication: NSEntityDescription? = managedObjectModel.entitiesByName["Publication"] as? NSEntityDescription {
publication?.renamingIdentifier = "Book"
}

Core Data takes care of migrating the data, but you still have the responsibility to update any code in your application that depends on the old name. To see this in practice, create a new version of your data model called Bookstore 3, based on model Bookstore 2, and set it as the current version. You should now be on version 3 (BookStore 3). In this version of the model, you’ll rename the Book entity to Publication. Go to your new model file, BookStore 3.xcdatamodel, and rename the Book entity to Publication. Then, go to the Versioning section in the Data Model inspector tab, and type the old name, Book, into the Renaming ID field so that Core Data will know how to migrate the existing data (as Figure 6-7 shows), and save this model.

image

Figure 6-7. Renaming Book to Publication and specifying the Renaming ID

Wait! Before running the application, remember that you’re responsible for changing any code that relies on the old name, Book. You could change the custom managed object class name from Book to Publication, change the relationship name in the Page entity from book topublication, and change any variable names appropriately. Let’s keep it simple here and change the places the BookStore application refers to the Book entity name. You find them in ViewController.m or in ViewController.swift, in the calls tofetchRequestWithEntityName: and insertNewObjectForEntityForName:. In those calls, change the entity name string from Book to Publication.

You can now build and run the BookStore application; all existing books should still exist. You can open the SQLite database and confirm that the schema now has a ZPUBLICATION table.

CREATE TABLE ZPUBLICATION ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZCATEGORY INTEGER, ZPRICE FLOAT, ZAUTHOR VARCHAR, ZTITLE VARCHAR);

and no ZBOOK table.

As you’ve seen, lightweight migrations are nearly effortless, at least for you. Core Data handles the difficult work of figuring out how to map and migrate the data from the old model to new. However, if your model changes don’t fit within what lightweight migrations can handle, you must specify your own mapping model.

Using a Mapping Model

When your data model changes exceed Core Data’s ability to infer how to map data from the old model to the new, you can’t use a lightweight migration to automatically migrate your data. Instead, you have to create “a mapping model” to tell Core Data how to execute the migration. This section walks you through the steps involved in creating a mapping model for your migration. A mapping model is roughly analogous to a data model—whereas a data model contains entities and properties, a mapping model, which is of type NSMappingModel, has entity mappings (of type NSEntityMapping) and property mappings (of type NSPropertyMapping). The entity mappings do just what you’d expect them to do: they map a source entity to a target entity. The property mappings, as well, do what you’d think: map source properties to target properties. The mapping model uses these mappings, along with their associated types and policies, to perform the migration.

Note Apple’s documentation and code use the terms “destination” and “target” interchangeably to refer to the new data model. In this chapter, we follow suit and use both “destination” and “target” interchangeably.

In a typical data migration, most entities and properties haven’t changed from the old version to the new. For these cases, the entity mappings simply copy each entity from the source model to the target model. When you create a mapping model, you’ll notice that Xcode generates these entity mappings, along with anything else it can infer from your model changes, and stores these mappings in the mapping model. These mappings represent how Core Data would have migrated your data in a lightweight migration. You have to create new mappings, or adjust the mappings Xcode generates, to change how your data migrate.

Understanding Entity Mappings

Each entity mapping, represented by an NSEntityMapping instance, contains three things.

· A source entity

· A destination entity

· A mapping type

When Core Data performs the migration, it uses the entity mapping to move the source to the destination, using the mapping type to determine how to do that. Table 6-1 lists the mapping types, their corresponding Core Data constants, and what they mean.

Table 6-1. The Entity Mapping Types

Type

Core Data Constant

Meaning

Add

NSAddEntityMappingType

The entity is new in the destination model—it doesn’t exist in the source model—and should be added.

Remove

NSRemoveEntityMappingType

The entity doesn’t exist in the destination model and should be removed.

Copy

NSCopyEntityMappingType

The entity exists in both the source and destination models unchanged and should be copied as is.

Transform

NSTransformEntityMappingType

The entity exists in both the source and destination models but with changes. The mapping tells Core Data how to migrate each source instance to a destination instance.

Custom

NSCustomEntityMappingType

The entity exists in both the source and destination models but with changes.

The mapping tells Core Data how to migrate each source instance to a destination instance. The Add, Remove, and Copy types don’t generate much interest because lightweight migrations handle these types of entity mappings. The Transform and Custom types, however, are what make this section of the book necessary. They tell Core Data that each source entity instance must be transformed, according to any specified rules, into an instance of the destination entity. You’ll see an example of both a Transform and a Custom entity mapping type in this chapter. If you specify a value expression for one of the entity’s properties, the entity mapping is of a Transform type. If you specify a custom migration policy for the entity mapping, the entity mapping becomes a Custom type. As you work through this chapter, pay attention to the types the Core Data mapping modeler makes to your mapping model in response to changes you make. To specify the rules for a Custom entity mapping type, you create a migration policy, which is a class you write that derives from NSEntityMigrationPolicy. You then set the class you create as the custom policy for the entity mapping in the Xcode mapping modeler.

Core Data runs your migration in three stages.

1. It creates the objects in the destination model, including their attributes, based on the objects in the source model.

2. It creates the relationships among the objects in the destination model.

3. It validates the data in the destination model and saves them.

You can customize how Core Data performs these three steps through the Custom Policy. The NSEntityMigrationPolicy class has seven methods you can override to customize how Core Data will migrate data from the source entity to the target entity, though you’ll rarely override all of them. These methods, listed in Apple’s documentation for the NSEntityMigrationPolicy class, provide various places during the migration that you can override and change Core Data’s migration behavior. You can override as few or as many of these methods as you’d like, though a custom migration policy that overrides none of the methods is pointless. Typically, you’ll override createDestinationInstancesForSourceInstance: if you want to change how destination instances are created or how their attributes are populated with data. You’ll overridecreateRelationshipsForDestinationInstance: if you want to customize how relationships between the destination entity and other entities are created. Finally, you’ll override performCustomValidationForEntityMapping: if you want to perform any custom validations during your migration.

The createDestinationInstancesForSourceInstance: method carries with it a caveat: if you don’t call the superclass’s implementation, which you probably won’t because you’re overriding this method to change the default behavior, you must call the migration manager’sassociateSourceInstance:withDestinationInstance:forEntityMapping: method to associate the source instance with the destination instance. Forgetting to do this will cause problems with your migration. You’ll learn the proper way to call this method later in this chapter, with the BookStore application.

Understanding Property Mappings

A property mapping, like an entity mapping, tells Core Data how to migrate source to destination. A property mapping is an instance of NSPropertyMapping and contains the following three things:

· The name of the property in the source entity

· The name of the property in the destination entity

· A value expression that tells Core Data how to get from source to destination

To change the way a property mapping migrates data, you provide the value expression for the property mapping to use. Value expressions follow the same syntax as predicates and use the following six predefined keys to assist with retrieving values:

· $manager, which represents the migration manager

· $source, which represents the source entity

· $destination, which represents the destination entity

· $entityMapping, which represents the entity mapping

· $propertyMapping, which represents the property mapping

· $entityPolicy, which represents the entity migration policy

As you can see, these keys have names that make deducing their purposes easy. When you create a mapping model, you can explore the property mappings Xcode infers from your source and target data models to better understand what these keys mean and how they’re used.

The simplest value expressions copy an attribute from the source entity to the destination entity. If you have a Person entity, for example, that has a name attribute and the Person entity hasn’t changed between your old and your new model versions, the value expression for the name attribute in your PersonToPerson entity mapping would be as follows:

$source.name

You can also perform manipulations of source data using value expressions. Suppose, for example, that the same Person entity had an attribute called salary that stores each person’s salary. In the new model, you want to give everyone 4% raises. Your value expression would look like the following:

$source.salary*1.04

Since properties represent both attributes and relationships, property mappings represent mappings for both attributes and relationships. The typical value expression for a relationship calls a function, passing the migration manager, the destination instances, the entity mapping, and the name of the source relationship. For example, if your old data model had a relationship called staff in the Person entity that represented everyone who reported to this person and your new model has renamed this relationship to reports, the value expression for the reports property would look like the following:

FUNCTION($manager, "destinationInstancesForEntityMappingNamed:sourceInstances:", "PersonToPerson", $source.staff)

Note that you use the $manager key to pass the migration manager, that you get the destination instances from the entity mapping for the source instances, that you pass the appropriate entity mapping (PersonToPerson), and that you pass the old relationship that you get from the$source key.

Creating a New Model Version That Requires a Mapping Model

Lightweight migrations are often enough and you really don’t need to get too far into the mechanics of Core Data migrations. There are times, however, when you need to get your hands a bit dirty and help the framework figure out what to do. This is the case, for example, when you realize you’ve been lazy and you have combined data into one field that should really be split into multiples. Think about the author field, for example, which is expected to contain the author’s full name (e.g., John Doe). If you suddenly realized you wanted to split this into a firstName and alastName field, you’d have to create a mapping model. This is exactly what we will do in this section.

Before we get to the migration, let’s spend some time populating the author field in the Publication table so it gives us some data to migrate.

Open ViewController.m or ViewController.swift and add a new method called populateAuthors, as shown in Listing 6-3 (Objective-C) and Listing 6-4 (Swift).

Listing 6-3. Populating Authors (Objective-C)

- (void)populateAuthors {
// 1. Create a list of author names to assign
NSArray *authors = @[@"John Doe", @"Jane Doe", @"Bill Smith", @"Jack Brown"];

// 2. Get all the publications from the data store
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Publication"];
NSArray *books = [self.managedObjectContext executeFetchRequest:fetchRequest error:nil];
for(int i=0; i<books.count; i++) {
Book *book = books[i];
book.price = 20+i;

// 3. Set the author using one of the names in the array we created
[book setValue:authors[i % authors.count] forKey:@"author"];
}

// 4. Commit everything to the store
[self saveContext];
}

Listing 6-4. Populating Authors (Swift)

func populateAuthors() {
// 1. Create a list of author names to assign
let authors = ["John Doe", "Jane Doe", "Bill Smith", "Jack Brown"]

// 2. Get all the publications from the data store
let fetchRequest = NSFetchRequest(entityName: "Publication")
let books = self.managedObjectContext?.executeFetchRequest(fetchRequest, error: nil)
if let books = books {
for var i = 0; i<books.count; i++ {
var book = books[i] as Book
book.price = 20 + Float(i)
// 3. Set the author using one of the names in the array we created
book.setValue(authors[i % authors.count], forKeyPath: "author")
}
}

// 4. Commit everything to the store
saveContext()
}

Note that we also change the price to make sure it is about $15 so that we don’t trip the validation rules we had put in place in the original version of BookStore.

Finally, edit the viewDidAppear: method so it looks like Listing 6-5 (Objective-C) or Listing 6-6 (Swift).

Listing 6-5. Making the Call to Populate Authors (Objective-C)

- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self populateAuthors];
[self showExampleData];
}

Listing 6-6. Making the Call to Populate Authors (Swift)

override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
if let managedObjectContext = self.managedObjectContext {
self.populateAuthors()
self.showExampleData()
}

}

Prior to running the app, you can see in the .sqlite database that the author is not populated.

sqlite> select ztitle,zauthor,zprice from zpublication;

ZTITLE ZAUTHOR ZPRICE
--------------- -------- ----------
The second book NULL 20.0
The third book NULL 21.0
The first book NULL 22.0
The fourth book NULL 23.0

Launch the application once and then run the same query again.

sqlite> select ztitle,zauthor,zprice from zpublication;

ZTITLE ZAUTHOR ZPRICE
--------------- -------- ----------
The second book John Doe 20.0
The third book Jane Doe 21.0
The first book Bill Smith 22.0
The fourth book Jack Brown 23.0

Now go back to viewDidAppear: and remove the call to populateAuthors:.

We are now ready to create the new version of the model. Go ahead and create the new version of your data model; you should be up to version 4. In the new model (BookStore 4.xcdatamodel), in the Publication entity, delete the author attribute and create two new attributes:firstName and lastName. Both should have the type String. Your model should look like the one shown in Figure 6-8.

image

Figure 6-8. Splitting the two fields in the new model

Let’s take the time to also make sure the entity class is up to date with the new fields. Open Book.h if you’re doing this project in Objective-C and add declarations for the two new properties, firstName and lastName, as Listing 6-7 shows. Then add dynamic accessors for those properties in Book.m, as Listing 6-8 shows. If you’re following along in Swift, add the code for firstName and lastName to Book.swift that Listing 6-9 shows.

Listing 6-7. Adding Properties for firstName and lastName to Book.h

@property (nonatomic, retain) NSString *firstName;
@property (nonatomic, retain) NSString *lastName;

Listing 6-8. Adding Dynamic Accessors to Book.m

@dynamic firstName;
@dynamic lastName;

Listing 6-9. Adding firstName and lastName to Book.swift

@NSManaged var firstName: String
@NSManaged var lastName: String

The model and entity class are both up to date. We now move on to the migration part.

Creating a Mapping Model

To create a mapping model, create a new file in Xcode. In the ensuing dialog box, select Core Data under iOS on the left, and select Mapping Model on the right, as shown in Figure 6-9. Click Next.

image

Figure 6-9. Creating a Mapping Model file

The next step is to select the source model. Select BookStore 3.xcdatamodel, as shown in Figure 6-10, and click Next. You are then asked to select the target model. Select BookStore 4.xcdatamodel for the destination, as shown in Figure 6-11, and click Next. The last step is to name the mapping model. Name it Model3To4.xcmappingmodel and click Create.

image

Figure 6-10. Selecting the source data model

image

Figure 6-11. Selecting the destination data model

You should now see Xcode with your mapping model created, including all the entity mappings and property mappings that Core Data could infer, as shown in Figure 6-12.

image

Figure 6-12. The new mapping model

Notice that Xcode has already done some work to help with the migration. Creating a mapping model puts you in the same place you’d be with a lightweight migration, with mappings that Core Data can infer from your source and target data models. Where it found entities to match up, it automatically set the new attribute value to be a copy of the source. Since we removed the author attribute and added two new ones, it doesn’t quite know what to do. This is where you come in.

The next step is to customize the mapping model to migrate publications. Start by creating the custom policy you’ll use, which is a subclass of NSEntityMigrationPolicy. Create a new Cocoa Touch class called PublicationToPublicationMigrationPolicy that subclassesNSEntityMigrationPolicy, as shown in Figure 6-13.

image

Figure 6-13. Creating a new migration policy

In the implementation file, either PublicationToPublicationMigrationPolicy.m or PublicationToPublicationMigrationPolicy.swift, you want to override the createDestinationInstancesForSourceInstance: method. While performing the migration, Core Data will call this method each time it goes to create a Publication instance from a source Publication instance. In your implementation, you create a Publication instance, copy the price and title, and separate the first name from the last name. Of course, our name splitting is a bit rudimentary, but that is off topic here. This simple implementation will perfectly illustrate what we are trying to do.

Finally, you tell the migration manager about the relationship between the source’s Publication instance and the target’s new Publication instance, which is an important step for the migration to occur properly. Listing 6-10 shows Objective-C version, and Listing 6-11 shows the Swift version.

Listing 6-10. Custom Migration Mapping (Objective-C)

- (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)sourceInstance entityMapping:(NSEntityMapping *)mapping manager:(NSMigrationManager *)manager error:(NSError **)error {
// Create the book managed object
NSManagedObject *book =
[NSEntityDescription insertNewObjectForEntityForName:[mapping destinationEntityName]

inManagedObjectContext:[manager destinationContext]];
[book setValue:[sourceInstance valueForKey:@"title"] forKey:@"title"];
[book setValue:[sourceInstance valueForKey:@"price"] forKey:@"price"];

// Get the author name from the source
NSString *author = [sourceInstance valueForKey:@"author"];

// Split the author name into first name and last name
NSRange firstSpace = [author rangeOfString:@" "];
NSString *firstName = [author substringToIndex:firstSpace.location];
NSString *lastName = [author substringFromIndex:firstSpace.location+1];

// Set the first and last names into the bbok
[book setValue:firstName forKey:@"firstName"];
[book setValue:lastName forKey:@"lastName"];

// Set up the association between the old Publication and the new Publication for the migration manager
[manager associateSourceInstance:sourceInstance withDestinationInstance:book forEntityMapping:mapping];
return YES;
}

Listing 6-11. Custom Migration Mapping (Swift)

override func createDestinationInstancesForSourceInstance(sInstance: NSManagedObject!, entityMapping mapping: NSEntityMapping!, manager: NSMigrationManager!, error: NSErrorPointer) -> Bool {
// Create the book managed object

var book = NSEntityDescription.insertNewObjectForEntityForName(mapping.destinationEntityName, inManagedObjectContext: manager.destinationContext) as NSManagedObject!

book.setValue(sInstance.valueForKey("title"), forKey: "title")
book.setValue(sInstance.valueForKey("price"), forKey: "price")

// Get the author name from the source
let author = sInstance.valueForKey("author") as String?

// Split the author name into first name and last name
let firstSpace = author?.rangeOfString(" ")
if let firstSpace = firstSpace {
let firstName = author?.substringToIndex(firstSpace.startIndex)
let lastName = author?.substringFromIndex(firstSpace.endIndex)

// Set the first and last names into the bbok
book.setValue(firstName, forKeyPath: "firstName")
book.setValue(lastName, forKeyPath: "lastName")
}

// Set up the association between the old Publication and the new Publication for the migration manager
manager.associateSourceInstance(sInstance, withDestinationInstance: book, forEntityMapping: mapping)
return true
}

Notice also that we didn’t copy over the publication relationships. The relationships are copied over by default in NSEntityMigrationPolicy’s createRelationshipsForDestinationInstance: method. By not overriding that method, we get Core Data to copy those over for us.

Now let’s go back to the mapping model. Select the entity mapping called PublicationToPublication. In the panel to the right, set the custom policy to PublicationToPublicationMigrationPolicy (i.e., the one we just created), as Figure 6-14 shows.

image

Figure 6-14. Setting the new migration policy into the mapping model

Your mapping model is ready to perform the migration. Just as with lightweight migrations, however, you need to add some code to the application delegate to tell Core Data to execute a migration.

Migrating Data

Creating the mapping model is essential to performing a migration that lightweight migrations can’t handle, but you still must perform the actual migration. This section explains how to do this and then update the code for the BookStore application to actually run the migration. By the end of this section, you will have a BookStore application that includes all the books it had before, but now the first name and last name of the authors are correctly split.

Telling Core Data to migrate from the old model to the new one using a mapping model you create is similar to telling Core Data to use a lightweight migration. As you learned earlier, the way to tell Core Data to perform a lightweight migration is to pass an options dictionary that containsYES or true for two keys.

· NSMigratePersistentStoresAutomaticallyOption

· NSInferMappingModelAutomaticallyOption

The first key tells Core Data to automatically migrate the data, and the second tells Core Data to infer the mapping model.

For a migration using a mapping model you created, you clearly want Core Data to still automatically perform the migration, so you still set the NSMigratePersistentStoresAutomaticallyOption to YES or true. Since you’ve created the mapping model, however, you don’t want Core Data to infer anything; you want it to use your mapping model. Therefore, you set NSInferMappingModelAutomaticallyOption to NO or false, or you leave it out of the options dictionary entirely.

How, then, do you specify the mapping model for Core Data to use to perform the migration? Do you set it in the options dictionary? Do you pass it somehow to the persistent store coordinator’s addPersistentStoreWithType: method? Do you set it into the persistent store coordinator? Into the managed object model? Into the context?

The answer, which may seem a little shocking, is that you do nothing. Core Data will figure it out. It searches through your mapping models, finds one that’s appropriate for migrating from the source to the destination, and uses it. This makes migrations almost criminally easy.

Running Your Migration

To run the migration of your BookStore data store, open the AppDelegate.m file or the AppDelegate.swift file and remove the NSInferMappingModelAutomaticallyOption key from the options dictionary. This is all you have to do to get Core Data to migrate your data using your mapping model, Model3To4.xcmappingmodel. Set your current model version to BookStore 4, if you haven’t already, and then build and run the BookStore application; you should see all the books you had in the database before you ran the migration. Only this time, if you go look at the data store, you will see the names properly split:

sqlite> select ztitle,zfirstname,zlastname,zprice from zpublication;

ZTITLE ZFIRSTNAME ZLASTNAME ZPRICE
--------------- ---------- ---------- ----------
The second book John Doe 20.0
The third book Jane Doe 21.0
The fourth book Jack Brown 23.0
The first book Bill Smith 22.0

Custom Migrations

In most data migration cases, using the default three-step migration process (create the destination objects, create the relationships, and validate and save) is sufficient. Data objects are created in the new data store from the previous version, then all relationships are created, and finally the data are validated and persisted. There are, however, some cases where you want to intervene in the middle of the migration. This is where you need custom migrations. Typically, you know you need to start thinking about custom migration when the data changes go beyond moving data from an old entity to a new one. This is the case, for example, when the order of migration matters or if you want the user to be made aware of the status of the migration progress.

Taking control of the migration process initialization means that you need to perform all the work that Core Data usually does for you. This means that you need to do the following:

· Make sure migration is actually needed.

· Set up the migration manager.

· Run your migration by splitting the model into independent parts.

This section goes through a custom migration example step by step to show you how to make a nontrivial migration work. As the migration occurs, we will display the progress in the console log.

To set the stage for this example. First create a new version of the data model. You should have BookStore 5.xcdatamodel at this point. In the new model, let’s make some change to trigger the migration. Open the Publication entity and add a new property of type String calledsynopsis. Make BookStore 5.xcdatamodel the current model.

Making Sure Migration Is Needed

To validate that a model is compatible with the persistent store, you use the isConfiguration:compatibleWithStoreMetadata: method of the NSManagedObjectModel class before adding the persistent store to the coordinator. The data store metadata can be retrieved by querying the coordinator using the metadataForPersistentStoreOfType:URL:error: method.

Open AppDelegate.m or AppDelegate.swift and edit the persistentStoreCoordinator getter, as shown in Listing 6-12 (Objective-C) or Listing 6-13 (Swift).

Listing 6-12. Determining Whether Migration is Needed (Objective-C)

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (_persistentStoreCoordinator != nil) {
return _persistentStoreCoordinator;
}

_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"BookStoreEnhanced.sqlite"];

NSError *error = nil;
NSDictionary *sourceMetadata = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType URL:storeURL error:&error];

NSManagedObjectModel *destinationModel = [_persistentStoreCoordinator managedObjectModel];

BOOL pscCompatible = [destinationModel isConfiguration:nil compatibleWithStoreMetadata:sourceMetadata];

if (!pscCompatible) {
// Migration is needed

// ... perform the migration here
}

if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
[self showCoreDataError];
}

return _persistentStoreCoordinator;
}

Listing 6-13. Determining Whether Migration is Needed (Swift)

lazy var managedObjectContext: NSManagedObjectContext? = {
// Initialize the managed object model
let modelURL = NSBundle.mainBundle().URLForResource("BookStoreSwift", withExtension: "momd")
let managedObjectModel = NSManagedObjectModel(contentsOfURL: modelURL!)

// Initialize the persistent store coordinator
let storeURL = AppDelegate.applicationDocumentsDirectory.URLByAppendingPathComponent("BookStoreEnhancedSwift.sqlite")

var error: NSError? = nil
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)

let sourceMetadata = NSPersistentStoreCoordinator.metadataForPersistentStoreOfType (NSSQLiteStoreType, URL: storeURL, error: nil)

let destinationModel = persistentStoreCoordinator.managedObjectModel
let pscCompatible = destinationModel.isConfiguration(nil, compatibleWithStoreMetadata: sourceMetadata)

if(!pscCompatible) {
// Custom migration is needed

// ... perform the migration here
}

if(persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: nil, error: &error) == nil) {
self.showCoreDataError()
return nil
}

var managedObjectContext = NSManagedObjectContext()
managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator

// Add the undo manager
managedObjectContext.undoManager = NSUndoManager()

return managedObjectContext
}()

If migration is not needed, then the persistent store can be registered with the coordinator as usual.

Setting Up the Migration Manager

Once it has been determined that migration is indeed needed, the next step is to set up the migration manager, which is a subclass of NSMigrationManager. To do that, you need to retrieve the model that matches the current persistent store: the previous version of the model. This is the model you use as the source model of your migration process. The source model can be obtained using the mergedModelFromBundles:forStoreMetadata: method, which looks for a model in the application main bundle that matches the given metadata.

Start with creating your custom migration manager by creating a new class that extends NSMigrationManager. In this example we will call it MyMigrationManager. Listing 6-14 shows the Objective-C header file for this new class, and Listing 6-15 shows the Objective-C implementation file. Listing 6-16 shows the Swift implementation. The implementation of the migration overrides the associateSourceInstance:withDestinationInstance:forEntityMapping: method, which is called during the execution of the migration policy when destination instances are created for source instances. Your implementation creates an instance that is being migrated and assigns the synopsis attribute during the migration by simply copying the title attribute.

Listing 6-14. MyMigrationManager.h

#import <CoreData/CoreData.h>

@interface MyMigrationManager : NSMigrationManager

@end

Listing 6-15. MyMigrationManager.m

#import "MyMigrationManager.h"@implementation MyMigrationManager- (void)associateSourceInstance:(NSManagedObject*)sourceInstance withDestinationInstance:(NSManagedObject *)destinationInstance forEntityMapping:(NSEntityMapping *)entityMapping {
[super associateSourceInstance:sourceInstance withDestinationInstance:destinationInstance forEntityMapping:entityMapping];

NSString *name = [entityMapping destinationEntityName];
if([name isEqualToString:@"Publication"]) { NSString *title = [sourceInstance valueForKey:@"title"];
[destinationInstance setValue:title forKey:@"synopsis"];
}
}@end

Listing 6-16. MyMigrationManager.swift

import Foundation
import CoreData

class MyMigrationManager : NSMigrationManager {
override func associateSourceInstance(sourceInstance: NSManagedObject, withDestinationInstance destinationInstance: NSManagedObject, forEntityMapping entityMapping: NSEntityMapping) {
super.associateSourceInstance(sourceInstance, withDestinationInstance: destinationInstance, forEntityMapping: entityMapping)

let name = entityMapping.destinationEntityName
if name == "Publication" {
let title = sourceInstance.valueForKey("title") as? String
destinationInstance.setValue(title, forKeyPath: "synopsis")
}
}
}

Running the Migration

In this last step, you need to set your own class as the manager in charge of performing this migration. For the Objective-C version of BookStore, add an import for MyMigrationManager.h at the top of the AppDelegate.h file. For both Objective-C and Swift, add a newmigrationManager property, like this:

@property (readonly, strong, nonatomic) MyMigrationManager *migrationManager; // Objective-C
var migrationManager: MyMigrationManager? // Swift

Finally, where we placed the comments that the migration was needed and to “perform the migration here,” add the code to actually perform the migration. Listing 6-17 shows the Objective-C code, and Listing 6-18 shows the Swift code.

Listing 6-17. Performing the migration (Objective-C)

if (!pscCompatible) {
// Migration is needed
NSManagedObjectModel *sourceModel = [NSManagedObjectModel mergedModelFromBundles:nil forStoreMetadata:sourceMetadata];
_migrationManager = [[MyMigrationManager alloc] initWithSourceModel:sourceModel destinationModel:[self managedObjectModel]];
[_migrationManager addObserver:self forKeyPath:@"migrationProgress" options:NSKeyValueObservingOptionNew context:NULL];
NSMappingModel *mappingModel = [NSMappingModel inferredMappingModelForSourceModel:sourceModel destinationModel:[self managedObjectModel] error:nil];

NSURL *tempURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent: @"BookStoreEnhanced-temp.sqlite"];

if(![_migrationManager migrateStoreFromURL:storeURL
type:NSSQLiteStoreType
options:nil
withMappingModel:mappingModel
toDestinationURL:tempURL
destinationType:NSSQLiteStoreType
destinationOptions:nil
error:&error]) {
// Deal with error
NSLog(@"%@", error);
}
else {
// Delete the old store, rename the new one
NSFileManager *fm = [NSFileManager defaultManager];
[fm removeItemAtPath:[storeURL path] error:nil];
[fm moveItemAtPath:[tempURL path] toPath:[storeURL path] error:nil];
}
}

Listing 6-18. Performing the migration (Swift)

if(!pscCompatible) {
// Custom migration is needed
let sourceModel = NSManagedObjectModel.mergedModelFromBundles([NSBundle.mainBundle()], forStoreMetadata: sourceMetadata!)

var appDelegate = UIApplication.sharedApplication().delegate as? AppDelegate

let migrationManager = MyMigrationManager(sourceModel: sourceModel!, destinationModel: managedObjectModel!)

migrationManager.addObserver(self, forKeyPath: "migrationProgress", options: .New, context: nil)

appDelegate?.migrationManager = migrationManager

let mappingModel = NSMappingModel.inferredMappingModelForSourceModel(sourceModel!, destinationModel:managedObjectModel!, error: nil)

let tempURL = AppDelegate.applicationDocumentsDirectory.URLByAppendingPathComponent ("BookStoreEnhancedSwift-temp.sqlite")

if !migrationManager.migrateStoreFromURL(storeURL, type: NSSQLiteStoreType, options:nil, withMappingModel:mappingModel, toDestinationURL:tempURL, destinationType:NSSQLiteStoreType, destinationOptions:nil, error:&error) {
self.showCoreDataError()
abort()
//return nil
}
else {
let fm = NSFileManager()

fm.removeItemAtPath(storeURL.path!, error: nil)
fm.moveItemAtPath(tempURL.path!, toPath: storeURL.path!, error: nil)
}
}

Note that the source and destination store URLs (uniform resource locators) are the same since we are migrating data for the same persistent store.

We also added a key/value observer to the NSMigrationManager to monitor the progress. Update the existing method to observe the value and log it. Listing 6-19 shows the Objective-C method, and Listing 6-20 shows the Swift function.

Listing 6-19. Monitoring the Progress of the Migration (Objective-C)

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if ([keyPath isEqualToString:@"migrationProgress"]) {
NSMigrationManager *manager = (NSMigrationManager*)object;
NSLog(@"Migration progress: %d%%", (int)(manager.migrationProgress * 100.0));
}
}

Listing 6-20. Monitoring the Progress of the Migration (Swift)

override func observeValueForKeyPath(keyPath: String!,
ofObject object: AnyObject,
change: [NSObject : AnyObject],
context: UnsafeMutablePointer<()>) {
if keyPath == "migrationProgress" {
let manager = object as NSMigrationManager
println(String(format: "Migration progress: %d%%",manager.migrationProgress * 100.0))
}
}

Run the application to execute the custom migration and you will see the migration progress in the console: BookStore[21313:70b] Migration progress: 11%BookStore[21313:70b] Migration progress: 22%BookStore[21313:70b] Migration progress: 33%BookStore[21313:70b] Migration progress: 44%BookStore[21313:70b] Migration progress: 55%BookStore[21313:70b] Migration progress: 66%BookStore[21313:70b] Migration progress: 77%BookStore[21313:70b] Migration progress: 88%BookStore[21313:70b] Migration progress: 100%

Summary

Although changing data models can be painful in other programming environments, Core Data makes changing your data models nearly painless. With its support for model versions, lightweight migrations, and mapping models for migrations that aren’t lightweight, you can change your data models with impunity and let Core Data make sure your users’ data stay intact. As with any part of your application, however, make sure you test your migrations. Test them extensively. Test them even more than you test your application code. Users can handle an occasional application crash, but if they upgrade to the latest version of your application and lose their data in the process, your application and reputation may never recover.