0
votes

I have enabled versioning of my Core Data model and have been using light weight migration. My code always tries to do lightweight migration and then if that fails because the model are incompatible it falls back to deleting all existing data and refetching from the server. So lightweight migration is just used for efficiency and isn't required for correctness.

What I want to do now is make a change to my model which in theory lightweight migration could handle but in fact I need new data from the server. I want to somehow flag the model and not upgradeable via lightweight migration. For example if a field name has not changed but the meaning of that field has changed in such a way that the old code is incompatible with the new code base. (This is just an example.)

Has anyone found a way to flag a two models as incompatible so lightweight migration won't upgrade them?

2

2 Answers

3
votes

I've struggled with the same problem before.

I have a method that will attempt to migrate data using Mapping Models which is what you should use if you're going to turn off lightweight migration.

If you aren't going to do a lot of fancy data mapping, xcode will automatically create a mapping model that will work exactly like lightweight migration. All you have to do is create a new "Mapping Model" file each time you add a new version to Core Data. Just go to "File -> New -> New File" and under Core Data there should be a Mapping Model template. Select it and choose the source and destination versions.

I don't have my code openly available on github so I'll just post the migration method here.

- (BOOL)progressivelyMigrateURL:(NSURL*)sourceStoreURL ofType:(NSString*)type toModel:(NSManagedObjectModel*)finalModel
{
    NSError *error = nil;

    // if store dosen't exist skip migration
    NSString *documentDir = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"];
    if(![NSBundle pathForResource:@"YongoPal" ofType:@"sqlite" inDirectory:documentDir])
    {
        migrationProgress = 1.0;
        [self performSelectorOnMainThread:@selector(updateMigrationProgress) withObject:nil waitUntilDone:YES];

        // remove migration view
        [self.migrationView performSelectorOnMainThread:@selector(setHidden:) withObject:[NSNumber numberWithBool:YES] waitUntilDone:YES];
        [self.migrationView performSelectorOnMainThread:@selector(removeFromSuperview) withObject:nil waitUntilDone:YES];

        self.migrationView = nil;
        self.migrationProgressLabel = nil;
        self.migrationProgressView = nil;
        self.migrationSpinner = nil;

        return YES;
    }

    //START:progressivelyMigrateURLHappyCheck
    NSDictionary *sourceMetadata = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:type URL:sourceStoreURL error:&error];

    if (!sourceMetadata)
    {
        return NO;
    }

    if ([finalModel isConfiguration:nil compatibleWithStoreMetadata:sourceMetadata])
    {
        migrationProgress = 1.0;
        [self performSelectorOnMainThread:@selector(updateMigrationProgress) withObject:nil waitUntilDone:YES];

        // remove migration view
        [self.migrationView performSelectorOnMainThread:@selector(setHidden:) withObject:[NSNumber numberWithBool:YES] waitUntilDone:YES];
        [self.migrationView performSelectorOnMainThread:@selector(removeFromSuperview) withObject:nil waitUntilDone:YES];

        self.migrationView = nil;
        self.migrationProgressLabel = nil;
        self.migrationProgressView = nil;
        self.migrationSpinner = nil;

        error = nil;
        return YES;
    }
    else
    {
        migrationProgress = 0.0;
        [self.migrationView performSelectorOnMainThread:@selector(setHidden:) withObject:NO waitUntilDone:YES];
        [self performSelectorOnMainThread:@selector(updateMigrationProgress) withObject:nil waitUntilDone:YES];        
    }
    //END:progressivelyMigrateURLHappyCheck

    //START:progressivelyMigrateURLFindModels
    //Find the source model
    NSManagedObjectModel *sourceModel = [NSManagedObjectModel mergedModelFromBundles:nil forStoreMetadata:sourceMetadata];
    if(sourceModel == nil)
    {
        NSLog(@"%@", [NSString stringWithFormat:@"Failed to find source model\n%@", [sourceMetadata description]]);
        return NO;
    }

    //Find all of the mom and momd files in the Resources directory
    NSMutableArray *modelPaths = [NSMutableArray array];
    NSArray *momdArray = [[NSBundle mainBundle] pathsForResourcesOfType:@"momd" inDirectory:nil];
    for (NSString *momdPath in momdArray)
    {
        NSAutoreleasePool *pool = [NSAutoreleasePool new];
        NSString *resourceSubpath = [momdPath lastPathComponent];
        NSArray *array = [[NSBundle mainBundle] pathsForResourcesOfType:@"mom" inDirectory:resourceSubpath];
        [modelPaths addObjectsFromArray:array];
        [pool drain];
    }

    NSArray* otherModels = [[NSBundle mainBundle] pathsForResourcesOfType:@"mom" inDirectory:nil];
    [modelPaths addObjectsFromArray:otherModels];

    if (!modelPaths || ![modelPaths count])
    {
        //Throw an error if there are no models
        NSMutableDictionary *dict = [NSMutableDictionary dictionary];
        [dict setValue:@"No models found in bundle" forKey:NSLocalizedDescriptionKey];

        //Populate the error
        error = [NSError errorWithDomain:@"com.yongopal.coredata" code:500 userInfo:dict];
        if([[self.prefs valueForKey:@"debugMode"] isEqualToString:@"Y"])
        {
            NSLog(@"error: %@", error);
        }
        return NO;
    }
    //END:progressivelyMigrateURLFindModels

    //See if we can find a matching destination model
    //START:progressivelyMigrateURLFindMap
    NSMappingModel *mappingModel = nil;
    NSManagedObjectModel *targetModel = nil;
    NSString *modelPath = nil;

    for(modelPath in modelPaths)
    {
        targetModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:[NSURL fileURLWithPath:modelPath]];
        mappingModel = [NSMappingModel mappingModelFromBundles:nil forSourceModel:sourceModel destinationModel:targetModel];

        //If we found a mapping model then proceed
        if(mappingModel)
        {
            break;
        }
        else
        {
            //Release the target model and keep looking
            [targetModel release];
            targetModel = nil;
        }
    }

    //We have tested every model, if nil here we failed
    if (!mappingModel)
    {
        NSMutableDictionary *dict = [NSMutableDictionary dictionary];
        [dict setValue:@"No mapping models found in bundle" forKey:NSLocalizedDescriptionKey];
        error = [NSError errorWithDomain:@"com.yongopal.coredata" code:500 userInfo:dict];
        if([[self.prefs valueForKey:@"debugMode"] isEqualToString:@"Y"])
        {
            NSLog(@"error: %@", error);
        }
        return NO;
    }
    //END:progressivelyMigrateURLFindMap

    //We have a mapping model and a destination model.  Time to migrate
    //START:progressivelyMigrateURLMigrate
    NSMigrationManager *manager = [[NSMigrationManager alloc] initWithSourceModel:sourceModel destinationModel:targetModel];

    // reg KVO for migration progress
    [manager addObserver:self forKeyPath:@"migrationProgress" options:NSKeyValueObservingOptionNew context:NULL];

    NSString *modelName = [[modelPath lastPathComponent] stringByDeletingPathExtension];
    NSString *storeExtension = [[sourceStoreURL path] pathExtension];
    NSString *storePath = [[sourceStoreURL path] stringByDeletingPathExtension];

    //Build a path to write the new store
    storePath = [NSString stringWithFormat:@"%@.%@.%@", storePath, modelName, storeExtension];
    NSURL *destinationStoreURL = [NSURL fileURLWithPath:storePath];

    if (![manager migrateStoreFromURL:sourceStoreURL type:type options:nil withMappingModel:mappingModel toDestinationURL:destinationStoreURL destinationType:type destinationOptions:nil error:&error])
    {
        if([[self.prefs valueForKey:@"debugMode"] isEqualToString:@"Y"])
        {
            NSLog(@"error: %@", error);
        }
        [targetModel release];
        [manager removeObserver:self forKeyPath:@"migrationProgress"];
        [manager release];
        return NO;
    }
    [targetModel release];
    [manager removeObserver:self forKeyPath:@"migrationProgress"];
    [manager release];
    //END:progressivelyMigrateURLMigrate

    //Migration was successful, move the files around to preserve the source
    //START:progressivelyMigrateURLMoveAndRecurse
    NSString *guid = [[NSProcessInfo processInfo] globallyUniqueString];
    guid = [guid stringByAppendingPathExtension:modelName];
    guid = [guid stringByAppendingPathExtension:storeExtension];
    NSString *appSupportPath = [storePath stringByDeletingLastPathComponent];
    NSString *backupPath = [appSupportPath stringByAppendingPathComponent:guid];

    NSFileManager *fileManager = [NSFileManager defaultManager];
    if (![fileManager moveItemAtPath:[sourceStoreURL path] toPath:backupPath error:&error])
    {
        if([[self.prefs valueForKey:@"debugMode"] isEqualToString:@"Y"])
        {
            NSLog(@"error: %@", error);
        }
        //Failed to copy the file
        return NO;
    }

    //Move the destination to the source path
    if (![fileManager moveItemAtPath:storePath toPath:[sourceStoreURL path] error:&error])
    {
        if([[self.prefs valueForKey:@"debugMode"] isEqualToString:@"Y"])
        {
            NSLog(@"error: %@", error);
        }
        //Try to back out the source move first, no point in checking it for errors
        [fileManager moveItemAtPath:backupPath toPath:[sourceStoreURL path] error:nil];
        return NO;
    }

    //We may not be at the "current" model yet, so recurse
    return [self progressivelyMigrateURL:sourceStoreURL ofType:type toModel:finalModel];
    //END:progressivelyMigrateURLMoveAndRecurse
}

This is an edited version of a method I got from some Core Data book I can't remember the title of. I wish I could give credit to the author. :S

Beware, I have some code in here that you should remove in your implementation. It's mostly stuff I use to update the view on the progress of the migration.

You can use this method like so:

NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"YongoPal.sqlite"];

// perform core data migrations if necessary
if(![self progressivelyMigrateURL:storeURL ofType:NSSQLiteStoreType toModel:self.managedObjectModel])
{
    // reset the persistent store on fail
    NSString *documentDir = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"];
    NSError *error = nil;
    [[NSFileManager defaultManager] removeItemAtPath:[NSBundle pathForResource:@"YongoPal" ofType:@"sqlite" inDirectory:documentDir] error:&error];
}
else
{
    NSLog(@"migration succeeded!");
}

Remember to remove the lightweight migration option before you use this.

0
votes

Apple's Core Data Model Versioning and Data Migration Programming Guide addresses what to do if "you have two versions of a model that Core Data would normally treat as equivalent that you want to be recognized as being different", which I think is what you're asking.

The answer is to set the versionHashModifier on one of your Entities or Properties in the new model.

In your example, you'd do that on the field whose meaning has changed.