Retired Document
Important: This sample code may not represent best practices for current development. The project may use deprecated symbols and illustrate technologies and techniques that are no longer recommended.
Sample Applications/Migration/Migrator/DataMigrator.m
/* |
File: DataMigrator.m |
Abstract: <<ABSTRACT CONTENT HERE>> |
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple |
Computer, Inc. ("Apple") in consideration of your agreement to the |
following terms, and your use, installation, modification or |
redistribution of this Apple software constitutes acceptance of these |
terms. If you do not agree with these terms, please do not use, |
install, modify or redistribute this Apple software. |
In consideration of your agreement to abide by the following terms, and |
subject to these terms, Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this original Apple software (the |
"Apple Software"), to use, reproduce, modify and redistribute the Apple |
Software, with or without modifications, in source and/or binary forms; |
provided that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the following |
text and disclaimers in all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or logos of Apple Computer, |
Inc. may be used to endorse or promote products derived from the Apple |
Software without specific prior written permission from Apple. Except |
as expressly stated in this notice, no other rights or licenses, express |
or implied, are granted by Apple herein, including but not limited to |
any patent rights that may be infringed by your derivative works or by |
other works in which the Apple Software may be incorporated. |
The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
POSSIBILITY OF SUCH DAMAGE. |
Copyright © 2005 Apple Computer, Inc., All Rights Reserved |
*/ |
#import "DataMigrator.h" |
#import "MigrationUtilities.h" |
/** DataMigrator performs the actual work of migrating existing data from a data store |
* based on the old data model to a newly created data store based on the new model. |
* |
* During the actual migration, independent core data stacks are used for the old and new |
* managed object data spaces. No managed objects are passed between these data stacks, |
* see normalizeCuisines:, migrateRecipesAndIngredients: and migrateChefs:. |
*/ |
@implementation DataMigrator |
- (NSManagedObjectModel *)oldManagedObjectModel { |
if (oldManagedObjectModel) { |
return oldManagedObjectModel; |
} |
oldManagedObjectModel = [MigrationUtilities retainedOldManagedObjectModel]; |
return oldManagedObjectModel; |
} |
- (NSManagedObjectModel *)newManagedObjectModel { |
if (newManagedObjectModel) { |
return newManagedObjectModel; |
} |
newManagedObjectModel = [MigrationUtilities retainedNewManagedObjectModel]; |
// Tweak the model so the recipe->chef relationship is no longer required |
// This is necessary because of the way we move the object graph from |
// V1 to V2 in pieces instead of all at once |
[[[[[newManagedObjectModel entitiesByName] valueForKey: @"Recipe"] relationshipsByName] valueForKey: @"chef"] setOptional: YES]; |
return newManagedObjectModel; |
} |
- (NSManagedObjectContext *)createOldCoreDataStackWithStorePath:(NSString *)storePath { |
if (oldManagedObjectContext) { |
return oldManagedObjectContext; |
} |
oldManagedObjectContext = [MigrationUtilities createRetainedCoreDataStackWithStorePath: storePath andModel: [self oldManagedObjectModel]]; |
return oldManagedObjectContext; |
} |
- (NSManagedObjectContext *)createNewCoreDataStackWithStorePath:(NSString *)newStorePath { |
if (newManagedObjectContext) { |
return newManagedObjectContext; |
} |
newManagedObjectContext = [MigrationUtilities createRetainedCoreDataStackWithStorePath: newStorePath andModel: [self newManagedObjectModel]]; |
// Update the metadata to V2 |
if (nil != newManagedObjectContext) { |
[MigrationUtilities setMetadata: @"Version 2" forKey: @"Migration Example Version" inStoreWithPath: newStorePath inContext: newManagedObjectContext]; |
} |
return newManagedObjectContext; |
} |
- (id)initWithOldStorePath:(NSString *)oldStorePath newStorePath:(NSString *)newStorePath { |
self = [super init]; |
if (nil != self) { |
[self createOldCoreDataStackWithStorePath: oldStorePath]; |
[self createNewCoreDataStackWithStorePath: newStorePath]; |
} |
return self; |
} |
- (void)dealloc { |
[newManagedObjectModel release]; |
newManagedObjectModel = nil; |
[newManagedObjectContext release]; |
newManagedObjectContext = nil; |
[oldManagedObjectModel release]; |
oldManagedObjectModel = nil; |
[oldManagedObjectContext release]; |
oldManagedObjectContext = nil; |
[super dealloc]; |
} |
// The first step in the migration process: iterate through the existing Recipes, and create |
// Cuisine objects encapsulating what used to be a string attribute. Also makes sure to unique |
// the Cuisine names, since we only want one instance each of "Cuisine A", "Cuisine B", and |
// "Cuisine C", even though they appear in more than one recipe |
- (BOOL)normalizeCuisines:(NSError **)error { |
NSFetchRequest *oldRecipesFetchRequest = [[[NSFetchRequest alloc] init] autorelease]; |
[oldRecipesFetchRequest setEntity: [[oldManagedObjectModel entitiesByName] valueForKey: @"Recipe"]]; |
NSArray *oldRecipes = [oldManagedObjectContext executeFetchRequest: oldRecipesFetchRequest error: error]; |
if (nil == *error) { |
NSArray *oldCuisines = [oldRecipes valueForKey: @"cuisine"]; |
NSArray *newCuisines = [NSMutableDictionary dictionary]; |
int i = 0, count = [oldCuisines count]; |
// Go through all old recipes' cuisine names and create new managed objects for each unique value |
for ( ; i < count ; i++ ) { |
NSString *cuisineName = [oldCuisines objectAtIndex: i]; |
if (nil == [newCuisines valueForKey: cuisineName]) { |
// We don't have a cuisine with this name yet, so create one |
NSManagedObject *cuisine = [NSEntityDescription insertNewObjectForEntityForName: @"Cuisine" inManagedObjectContext: newManagedObjectContext]; |
[cuisine setValue: cuisineName forKey: @"name"]; |
[newCuisines setValue: cuisine forKey: cuisineName]; |
} |
} |
[newManagedObjectContext save: error]; |
if (nil !=* error) { |
NSLog(@"Cuisine normalization error"); |
return NO; |
} |
return YES; |
} |
return NO; |
} |
// The second step: migrate all of the Recipes and their Ingredients. Interesting features here |
// are the use of the default value defined in the model for the new 'rating' attribute on the |
// Recipe entity, and the use of the fetch request to find the new instance of the correct |
// Cuisine object for each recipe as created in the previous step. We take advantage of the |
// model modification made in newManagedObjectModel to leave the Chef relationship nil for now |
// even though the model specifies that it is required. This relationship will be connected |
// in the next step. |
// The Ingredient transform is simple: nothing in the model changed, so we simply copy over the |
// new values. |
- (BOOL)migrateRecipesAndIngredients:(NSError **)error { |
NSFetchRequest *oldRecipesFetchRequest = [[[NSFetchRequest alloc] init] autorelease]; |
[oldRecipesFetchRequest setEntity: [[oldManagedObjectModel entitiesByName] valueForKey: @"Recipe"]]; |
NSArray *oldRecipes = [oldManagedObjectContext executeFetchRequest: oldRecipesFetchRequest error: error]; |
if (nil == *error) { |
int i = 0, recipeCount = [oldRecipes count]; |
// There's a default recipe rating value in the new model, so use it when we do the switch |
NSNumber *defaultRating = [[[[[newManagedObjectModel entitiesByName] valueForKey: @"Recipe"] attributesByName] valueForKey: @"rating"] defaultValue]; |
// Set up a fetch request that I can reuse to find the new cuisines objects for my recipes |
NSFetchRequest *cuisineFetchRequest = [[[NSFetchRequest alloc] init] autorelease]; |
[cuisineFetchRequest setEntity: [[newManagedObjectModel entitiesByName] valueForKey: @"Cuisine"]]; |
for ( ; i < recipeCount ; i++ ) { |
NSManagedObject *oldRecipe = [oldRecipes objectAtIndex: i]; |
NSManagedObject *newRecipe = [NSEntityDescription insertNewObjectForEntityForName: @"Recipe" inManagedObjectContext: newManagedObjectContext]; |
// Copy values that haven't changed over |
[newRecipe setValue: [oldRecipe valueForKey: @"name"] forKey: @"name"]; |
[newRecipe setValue: [oldRecipe valueForKey: @"directions"] forKey: @"directions"]; |
// New value for new attribute |
[newRecipe setValue: defaultRating forKey: @"rating"]; |
// Find the cuisine containing the value we normalized out in step 1 and connect it |
[cuisineFetchRequest setPredicate: [NSPredicate predicateWithFormat: [NSString stringWithFormat: @"name = \"%@\"", [oldRecipe valueForKey: @"cuisine"]]]]; |
NSArray *cuisines = [newManagedObjectContext executeFetchRequest: cuisineFetchRequest error: error]; |
if (nil !=* error) { |
NSLog(@"Can't find cuisine error"); |
} |
[[newRecipe mutableSetValueForKey: @"cuisines"] addObject: [cuisines objectAtIndex: 0]]; |
// Now get the ingredients |
NSEnumerator *oldIngredientsEnumerator = [(NSSet *)[oldRecipe valueForKey: @"ingredients"] objectEnumerator]; |
NSManagedObject *oldIngredient = nil; |
while (oldIngredient = (NSManagedObject *)[oldIngredientsEnumerator nextObject]) { |
NSManagedObject *newIngredient = [NSEntityDescription insertNewObjectForEntityForName: @"Ingredient" inManagedObjectContext: newManagedObjectContext]; |
// And do the copying, since nothing interesting changed here |
[newIngredient setValue: [oldIngredient valueForKey: @"name"] forKey: @"name"]; |
[newIngredient setValue: [oldIngredient valueForKey: @"amount"] forKey: @"amount"]; |
[[newRecipe mutableSetValueForKey: @"ingredients"] addObject: newIngredient]; |
} |
} |
[newManagedObjectContext save: error]; |
if (nil !=* error) { |
NSLog(@"Recipe/ingredients save error"); |
return NO; |
} |
return YES; |
} |
return NO; |
} |
// Creates new Chef instances in the new context, and populates them with the data |
// from the old store. The previous 'name' attribute is separated into 'firstName' and |
// 'lastName' subcomponents, the 'training' attribute is dropped, and the Recipes |
// relationship is populated from the results of a fetch request in the new store. |
// Note that this relationship changed between models, in V1 it was to-one, in V2 it |
// is to-many. |
- (BOOL)migrateChefs:(NSError **)error { |
NSFetchRequest *oldChefFetchRequest = [[[NSFetchRequest alloc] init] autorelease]; |
[oldChefFetchRequest setEntity: [[oldManagedObjectModel entitiesByName] valueForKey: @"Chef"]]; |
[oldChefFetchRequest setSortDescriptors: [NSArray arrayWithObject: [[[NSSortDescriptor alloc] initWithKey: @"name" ascending: YES] autorelease]]]; |
NSArray *oldChefs = [oldManagedObjectContext executeFetchRequest: oldChefFetchRequest error: error]; |
if (nil == *error) { |
int chefCount = [oldChefs count]; |
int i = 0; |
NSFetchRequest *newChefRecipeFetchRequest = [[[NSFetchRequest alloc] init] autorelease]; |
[newChefRecipeFetchRequest setEntity: [[newManagedObjectModel entitiesByName] valueForKey: @"Recipe"]]; |
for ( ; i < chefCount ; i++ ) { |
NSManagedObject *oldChef = [oldChefs objectAtIndex: i]; |
NSManagedObject *oldChefRecipe = [oldChef valueForKey: @"recipes"]; |
// predicate to find the new version of this chef's recipe, since we've already created and saved it |
[newChefRecipeFetchRequest setPredicate: [NSPredicate predicateWithFormat: [NSString stringWithFormat: @"name = \"%@\"", [oldChefRecipe valueForKey: @"name"]]]]; |
NSArray *newChefRecipe = [newManagedObjectContext executeFetchRequest: newChefRecipeFetchRequest error: error]; |
if (nil != *error) { |
NSLog(@"Can't find recipe %@" , [oldChefRecipe valueForKey: @"name"]); |
} |
NSManagedObject *newChef = [NSEntityDescription insertNewObjectForEntityForName: @"Chef" inManagedObjectContext: newManagedObjectContext]; |
NSString *oldChefName = [oldChef valueForKey: @"name"]; |
NSArray *oldChefNameComponents = [oldChefName componentsSeparatedByString: @", "]; |
// Split the previous 'name' attribute into the firstName and lastName parts |
[newChef setValue: [oldChefNameComponents objectAtIndex: 0] forKey: @"lastName"]; |
[newChef setValue: [oldChefNameComponents objectAtIndex: 1] forKey: @"firstName"]; |
// Don't copy over training, since we've dropped it from the model |
// Recipes was a to-one relationship, now it's a to-many, so we need to put it into the mutableSetValueForKey |
[[newChef mutableSetValueForKey: @"recipes"] addObject: [newChefRecipe objectAtIndex: 0]]; |
} |
[newManagedObjectContext save: error]; |
if (nil !=* error) { |
NSLog(@"Chef fetch error"); |
return NO; |
} |
return YES; |
} |
return NO; |
} |
- (BOOL)migrateData:(NSError **)error { |
if ([self normalizeCuisines: error]) { |
if ([self migrateRecipesAndIngredients: error]) { |
if ([self migrateChefs: error]) { |
return YES; |
} |
} |
} |
return NO; |
} |
@end |
Copyright © 2005 Apple Computer, Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2005-06-01