Your First Data Model Version
3.4 A Heavy/Manual Migration
A heavy migration is required when we go outside of the bounds of what a light migration can accomplish. A simple example of something that goes beyond a light migration is a logic-based migration. Imagine a situation in which, as part of a migration/application update, we need to change the data that is in the database. Perhaps there is a typo in the included data or a change in the logic. Changing the data during the migration is outside of the scope of a light migration. However, we can easily add it to a heavy migration.
A more complex example would be a situation that involves normalizing data.
In our application, each recipe has one or more recipe ingredients. If we wanted to expand our application and extract the common parts of the recipe ingredient into new tables, we would be stepping outside the boundaries of what a light migration can accomplish.
Creating Our First Mapping Model
In this migration, we are going to accomplish two goals. First we are going to massage the data during the migration and find every occurrence of Meat or Fish for the recipe type and replace it with Entrée. In addition, we are going to create new entities: Ingredient and UnitOfMeasure. During the migration of the RecipeIngredient entity, we are going to create or associate the appropriate Ingre-dient to the RecipeIngredient.
The first step is to create a mapping model for this migration. A mapping model is a description of how the migration is supposed to work. First we create a new file in Xcode, and select the section for Core Data. In that section, there is a template called Mapping Model, as shown in Figure 10, Mapping Model template, on page 45. Once we select that template, Xcode asks us to choose the data model version to use as the source. For this migration, we are going from v2 to v3, so we select v2. Then Xcode asks us to select the destination model; we’ll select v3. Finally, we must name the mapping model. I named it the very descriptive FromV2toV3.
Figure 10—Mapping Model template
Now Xcode will do a best guess at the migration from v2 to v3 and display its results, as shown in Figure 11, Completed mapping model, on page 46. There is a lot of information in this view; let’s go through it piece by piece. As we step through this view, keep in mind that the view represents the migration from the perspective of the destination model. Everything is described as coming from the source into the destination.
The entity mappings are on the left side of the view, just to the right of the project list. Each item in this list represents a part of the migration that will occur. They’re not one-to-one with the number of entities that we have; we can actually have more or less mappings than we have entities. Specifically, as we will see when we update this mapping model, we can have more than one mapping for an entity. In addition, if an entity is no longer represented in the new model, it may not have a mapping at all.
To the right of the entity mappings are the attribute and relationship mappings.
The attribute mappings are located at the top of the view. This section of the view describes how the attributes of an entity are mapped from the source to the destination. Since we are looking at this from the perspective of the des-tination, this list includes every attribute that exists in the destination entity.
A Heavy/Manual Migration
•
45Figure 11—Completed mapping model
Xcode has already prepopulated the attributes and taken a guess at where the attributes come from. Reviewing what has already been populated by Xcode, we can see how the attribute mappings work. Several variables are available for these mappings:
• $manager, which references the NSMigrationManagerKey
• $source, which references the NSMigrationSourceObjectKey
• $destination, which references the NSMigrationDestinationObjectKey
• $entityMapping, which references the NSMigrationEntityMappingKey
• $propertyMapping, which references the NSMigrationPropertyMappingKey
• $entityPolicy, which references the NSMigrationEntityPolicyKey
The relationship mappings are below the entity mappings. Like the attribute mappings, these resolve the relationships for the destination entity. Normally, these mappings resolve to an instance of an entity in the destination store that existed in the source store. To accomplish this, the mapping for the object at the other end of the relationship must be higher in the list (the list is migrated in order, top to bottom). Then it is a matter of configuring the map-ping properly, as shown in Figure 12, Relationship mapmap-ping, on page 47.
Customizing the Heavy/Manual Migration
So far, the migration we built doesn’t do very much: it migrates the entities from the old store to the new store. We need to adjust the migration to make it perform the more complicated aspects of the migration, the ones that are beyond the abilities of the light migration.
Figure 12—Relationship mapping
The first part of this customization is to change the Type attribute on recipes that are currently set as Fish or Meat. To do this, we need to utilize the filter feature of the migration. First, select the RecipeToRecipe mapping. Next, open the Mapping Model inspector, which is utility view 3 in Xcode (DE3). In that inspector, we see a Filter Predicate field. This filter determines which entities are migrated. Utilizing a filter, we can migrate only those recipe entities that have their type set to something other than Fish or Meat. The setting is shown here:
A Heavy/Manual Migration
•
47However, this leaves our meat and fish recipes unmigrated. To include them, we first duplicate the RecipeToRecipe mapping (unfortunately by hand with the current version of Xcode) and change the filter to be inclusive of Meat and Fish. Then, in this second mapping, we change the Value Expression setting for the Type attribute to Entrée. Taking this step will migrate all of the meat and fish recipes and change the type to Entrée.
The second part of this migration is far more complex. For each RecipeIngredient, we want to either create a new Ingredient entity or link the destination RecipeIn-gredient to an existing Ingredient. In addition, for each Ingredient that we create, we want to create a UnitOfMeasure entity to go with it.
To complete this very complex migration, we are going to need to resort to code. The mapping model editor is not capable of handling the complexity we need for this next bit of the migration. Fortunately, we can step in and write the code to handle the migration directly. To set this up, we start off by modifying the mapping model. The first step is to delete the mappings for Ingredient and UnitOfMeasure. There is no corresponding entity in the source model, so no mapping is needed.
Next, we need to modify the RecipeIngredientToRecipeIngredient. After selecting the RecipeIngredientToRecipeIngredient mapping, we need to look at the Mapping Model inspector and set a custom policy. The Custom Policy field tells the migration that instead of using its built-in migration policy (which will read from the mapping model), we are going to introduce custom code. This field accepts a class name that we are going to set to RecipeIngredientToIngredientAndUnitOfMeasure. NSEntityMigrationPolicy
Core Data will instantiate an instance of NSEntityMigrationPolicy for each mapping in the mapping model. NSEntityMigrationPolicy is designed to be subclasses so that we can override all or part of the migration. There are several methods that can be overridden in the subclass; in this example, we are overriding two methods.
- (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject*)source entityMapping:(NSEntityMapping*)mapping
manager:(NSMigrationManager*)manager error:(NSError**)error
- (BOOL)createRelationshipsForDestinationInstance:(NSManagedObject*)dInstance entityMapping:(NSEntityMapping*)mapping
manager:(NSMigrationManager*)manager error:(NSError**)error
createDestinationInstancesForSourceInstance:
The first method, createDestinationInstancesForSourceInstance:, is called for each entity in the source store that is associated with this migration policy. For example, during the migration of the RecipeIngredient entities and the creation of the Ingredient and UnitOfMeasure entities, this method would be called for each RecipeIngredient, and it would be expected that at least an Ingredient entity would be created or associated with the incoming RecipeIngredient as a result.
The code to implement this breaks down as follows:
RecipesV3/PPRecipes/RecipeIngredientToIngredient.m
In the first part of the method, we are simply setting up references that will be needed later. Specifically, we are getting a reference to the destination NSManagedObjectContext, which we will need to create new entities, the name of the destination entity, and, most importantly, the name value from the incoming entity.
Since the incoming entity is a RecipeIngredient, the name value will be the name of the ingredient that we now want to reference.
RecipesV3/PPRecipes/RecipeIngredientToIngredient.m
A Heavy/Manual Migration
•
49In this next section of code, we deal with the possibility that the Ingredient entity that we need to reference has already been created. Rather than doing a fetch against the destination context every time, we have a hash built up and stored within the NSMigrationManger. The NSMigrationManager has an NSDictionary called userInfo that is perfectly suited for this purpose. We first lazily initialize this dictionary, and then we lazily initialize another NSDictionary inside it to store references to the Ingredient entities using the name of the ingredient as the key. With this, we can make sure that each Ingredient is created only once.
For each Ingredient, we also need to create or reference a UnitOfMeasure. We also grab a reference to the UnitOfMeasure lookup or create it if it has not been created yet.
Next we attempt to locate the Ingredient in the lookup dictionary. If it is not in the dictionary, we must create it and place it in the dictionary. If we need to create the Ingredient, we must resolve the UnitOfMeasure as well. Again, if it does not exist, we create it and put a reference to it in the lookup dictionary.
RecipesV3/PPRecipes/RecipeIngredientToIngredient.m
{
return YES;
}
@end
The last thing that we need to do is to tell the manager about the association.
Since the manager keeps track of all associations between the two NSManage-dObjectContext objects, we need to inform it of this new entity that was just created and that it is associated with the source entity that was passed in.
Once that is complete, we return YES, and we are done.
createRelationshipsForDestinationInstance:
In a properly designed data model, this method will rarely, if ever, be needed.
The intention of this method (which is called in the second pass) is to build any relationships for the new destination entity that was created in the previ-ous method. However, if all the relationships in the model are double-sided, this method is not necessary because we already set up one side of them. If for some reason there is an entity in the model that is not double-sided, additional code would be required in this method to handle the one-sided relationship. Since we do not need that functionality in our model, we just return YES.
RecipesV3/PPRecipes/RecipeIngredientToIngredient.m
- (BOOL)createRelationshipsForDestinationInstance:(NSManagedObject*)dInstance entityMapping:(NSEntityMapping*)mapping
manager:(NSMigrationManager*)manager error:(NSError**)error
{
return YES;
}
If you are thinking this is a lot of work for migration, well, you’re right. Man-ual migrations require a great deal of effort on the part of the developer, and there isn’t a lot of benefit to doing one. This, plus their poor performance, is the reason for my recommendation to avoid them at nearly any cost. However, no matter how hard we try to avoid it, sometimes heavy migration is the only answer. Fortunately, as we just examined, the option is available.