Projects
First, the store does not really matter. You can migrate from one type of store to another or to two different stores of the same type.
The basic workflow is simple, load the old context, load the new context, walk through every single object in the old context and save it in the new context. Fortunately this is pretty easy to do.
The way I solved it was to roll up most of the migration code. The class hieharchy is as follows:
NSManagedObject? --> ZDSManagedObject? --> My extended objects
So, in my model, if the data object does not require anything special then it is a ZDSManagedObject?, otherwise it extends ZDSManagedObject? into a child class.
Inside of the ZDSManagedObject? there are two methods that are used in the migration code:
- (void)copyFromManagedObject:(id)object withReference:(NSMutableDictionary *)reference; - (void)copyRelationshipsFromManagedObject:(id)object withReference:(NSMutableDictionary *)reference;
in the delegate (whether for your app or a document) before the context is loaded, check its metadata for a version number. If you don't have a version number yet, just assume that one is zero :)
NSDictionary *metadata; metadata = [NSPersistentStoreCoordinator metadataForPersistentStoreWithURL:loadURL error:&error];
I store the current version of the model in the application's plist which I check against this dictionary. Now assuming that the context needs to be updated, we step into it. First we load the old model, I called my model "Company" and version them with simple numbers (e.g. "Company1", "Company2", etc). So I load up the old context with the old model and the new context with the current model:
NSString *oldModelName = [NSString stringWithFormat:@"Company%u", buildNumber];
NSString *tempFilePath = [@"/tmp" stringByAppendingPathComponent:[NSString stringWithFormat:@"Company%u.seSales", currentBuildNumber]];
if ([[NSFileManager defaultManager] fileExistsAtPath:tempFilePath]) {
[[NSFileManager defaultManager] removeFileAtPath:tempFilePath handler:nil];
}
NSURL *newStoreURL = [NSURL fileURLWithPath:tempFilePath];
NSURL *momURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"Company" ofType:@"mom"]];
NSManagedObjectModel *newMoM = [[NSManagedObjectModel alloc] initWithContentsOfURL:momURL];
momURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:oldModelName ofType:@"mom"]];
NSManagedObjectModel *oldMoM = [[NSManagedObjectModel alloc] initWithContentsOfURL:momURL];
NSPersistentStoreCoordinator *oldCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:oldMoM];
NSPersistentStoreCoordinator *newCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:newMoM];
id store = [oldCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:companyURL options:nil error:&error];
NSAssert(store != nil, ([NSString stringWithFormat:@"Error initializing old store: %@", error]));
store = [newCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:newStoreURL options:nil error:&error];
NSAssert(store != nil, ([NSString stringWithFormat:@"Error initializing new store: %@", error]));
NSMutableDictionary *newStoreMetadata = [[NSMutableDictionary alloc] init];
[newStoreMetadata setValue:[NSNumber numberWithInt:currentBuildNumber] forKey:METADATA_BUILD_NUMBER];
[newCoordinator setMetadata:newStoreMetadata forPersistentStore:store];
NSManagedObjectContext *oldMOC = [[NSManagedObjectContext alloc] init];
NSManagedObjectContext *newMOC = [[NSManagedObjectContext alloc] init];
[oldMOC setPersistentStoreCoordinator:oldCoordinator];
[newMOC setPersistentStoreCoordinator:newCoordinator];
[oldCoordinator release];
[newCoordinator release];
NSDictionary *oldEntityDict = [oldMoM entitiesByName];
[newMoM release];
[oldMoM release];
The next step is to walk through every object in the old context:
NSEnumerator *entityNamesEnum = [[oldEntityDict allKeys] objectEnumerator];
Now for each entity, we step through and do a fetch to retrieve them all into memory. For each object in the old context you insert an object into the new context. After the new object is inserted, call -copyFromManagedObject:withReference: on the object passing in the NSMutableDictionary? that will hold all of the relationships.
The guts of -copyFromManagedObject:withReference: are as follows:
- (void)copyFromManagedObject:(id)object withReference:(NSMutableDictionary *)reference
{
NSEntityDescription *entity = [object entity];
NSArray *attributeKeys = [[entity attributesByName] allKeys];
NSDictionary *attributeValues = [object dictionaryWithValuesForKeys:attributeKeys];
[self setValuesForKeysWithDictionary:attributeValues];
[self copyRelationshipsFromManagedObject:object withReference:reference];
//Add myself to the relationship dictionary
[reference setValue:self forKey:[[[object objectID] URIRepresentation] absoluteString]];
}
- (void)copyRelationshipsFromManagedObject:(id)object withReference:(NSMutableDictionary *)reference
{
NSEntityDescription *entity = [object entity];
NSDictionary *relationships = [entity relationshipsByName];
NSEnumerator *relationshipEnum = [[relationships allKeys] objectEnumerator];
NSString *relationshipName;
while (relationshipName = [relationshipEnum nextObject]) {
NSRelationshipDescription *relationshipDescription = [relationships valueForKey:relationshipName];
if ([relationshipDescription isToMany]) {
//To many relationship
NSEnumerator *toManyEnum = [[object valueForKey:relationshipName] objectEnumerator];
NSManagedObject *toMany;
NSMutableSet *toManySet = [self mutableSetValueForKey:relationshipName];
while (toMany = [toManyEnum nextObject]) {
NSString *uid = [[[toMany objectID] URIRepresentation] absoluteString];
ZDSManagedObject *toManyNew = [reference valueForKey:uid];
if (toManyNew) {
[toManySet addObject:toManyNew];
}
}
} else {
//To one relationship
//see if the receiver has already been copied, if so link
NSString *uid = [[[[object valueForKey:relationshipName] objectID] URIRepresentation] absoluteString];
if (uid) {
ZDSManagedObject *toOne = [reference valueForKey:uid];
if (toOne) [self setValue:toOne forKey:relationshipName];
}
}
}
}
And that's it. There is of course memory issues to take into consideration, threading and other fun stuff but this is the basics of it minus anything that I missed. In a situation where you need to transition data from one example to another, you just override the appropriate method in your NSManagedObject? child object and handle it there.