10
votes

I'm trying to perform an iOS Core Data migration that requires a MappingModel. Core data is not able to use the mapping model for some reason and it falls back to an automatic lightweight migration.

I enabled the MigrationDebug option to get more info and what I see makes no sense. The source and destination hashes of the mapping model are identical, ignoring order, to the source and destination ManagedObjectModels. It seems like the mapping model should be used but the log says "no suitable mapping model found".

Here is the (elided) log:

CoreData: annotation: (migration)    will attempt automatic schema migration
CoreData: annotation: (migration) looking for mapping model with 
 source hashes: 
{
    TSBaseEntity = <4797118c 50068f2f f544d9a9 4884720b 55ec7e4d 0d4c8f4e 1ee44be3 b06d2edc>;
    TSBuyer = <91e837d1 3f348913 eff634d6 6fb9b3a6 747e2390 fbdc4ae6 32cc56d6 7582d4a8>;
    ...
}
 destination hashes: {
    TSBaseEntity = <4797118c 50068f2f f544d9a9 4884720b 55ec7e4d 0d4c8f4e 1ee44be3 b06d2edc>;
    TSBuyer = <e316a857 8919c4be eef15387 5c67a21b 67d32919 99ead438 1ff93c05 2e065fcc>;
    ...
}
CoreData: annotation: (migration) checking mapping model at path file://localhost/Users/xandrews/Library/Application%20Support/iPhone%20Simulator/6.1/Applications/0A84951E-21FC-47C0-A1B7-F880ACB672C4/Dev.app/Migrate_0_5_24To_0_5_27.cdm
 source hashes: 
{(
    <4797118c 50068f2f f544d9a9 4884720b 55ec7e4d 0d4c8f4e 1ee44be3 b06d2edc>,
    <91e837d1 3f348913 eff634d6 6fb9b3a6 747e2390 fbdc4ae6 32cc56d6 7582d4a8>,
    ...
)}
 destination hashes: {(
    <4797118c 50068f2f f544d9a9 4884720b 55ec7e4d 0d4c8f4e 1ee44be3 b06d2edc>,
    <e316a857 8919c4be eef15387 5c67a21b 67d32919 99ead438 1ff93c05 2e065fcc>,
    ...
)}
CoreData: annotation: (migration) no suitable mapping model found
CoreData: annotation: (migration) inferring a mapping model between data models with 
 source hashes: ...
5

5 Answers

8
votes

The mapping model generated by Xcode 4 does not produce the correct hashes needed for the migration to occur. You can compare the output from the migration log to your mapping file's hashes with the code below:

    NSString *mappingModelPath = [[NSBundle mainBundle] pathForResource:@"MappingFile" ofType:@"cdm"];
    NSMappingModel *mappingModel = [[NSMappingModel alloc] initWithContentsOfURL:[NSURL fileURLWithPath:mappingModelPath]];

    for (NSEntityMapping *entityMapping in mappingModel.entityMappings) {
        NSLog(@"%@: %@", entityMapping.sourceEntityName, entityMapping.sourceEntityVersionHash);
        NSLog(@"%@: %@", entityMapping.destinationEntityName, entityMapping.destinationEntityVersionHash);
    }

You'll see that these do not match the hashes in the migration log output.

The workaround is to generate the mapping file in Xcode 5.

3
votes

In your persistentStoreCoordinator method give this line of code

 NSDictionary *options=[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES],NSMigratePersistentStoresAutomaticallyOption,[NSNumber numberWithBool:YES],NSInferMappingModelAutomaticallyOption, nil];

If this doesnt help then you need to go for user implemented migration. So you will have to create a mapping model using source and destination model. In that case set,

NSDictionary *options=[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES],NSMigratePersistentStoresAutomaticallyOption,[NSNumber numberWithBool:NO],NSInferMappingModelAutomaticallyOption, nil];

Create douce metadata with following code

if (sourceMetadata) {
        NSString *configuration = nil;
        NSManagedObjectModel *destinationModel = [self.persistentStoreCoordinator managedObjectModel];

        //Our Source 1 is going to be incompatible with the Version 2 Model, our Source 2 won't be...
        BOOL pscCompatible = [destinationModel isConfiguration:configuration compatibleWithStoreMetadata:sourceMetadata];
        NSLog(@"Is the STORE data COMPATIBLE? %@", (pscCompatible==YES) ?@"YES" :@"NO");

        if (pscCompatible == NO) {
            migrationSuccess = [self performMigrationWithSourceMetadata:sourceMetadata toDestinationModel:destinationModel];
        }
    }
    else {
        NSLog(@"checkForMigration FAIL - No Source Metadata! \nERROR: %@", [error localizedDescription]);
    }

and implement the following function

- (BOOL)performMigrationWithSourceMetadata :(NSDictionary *)sourceMetadata toDestinationModel:(NSManagedObjectModel *)destinationModel
{
    BOOL migrationSuccess = NO;
    //Initialise a Migration Manager...
    NSManagedObjectModel *sourceModel = [NSManagedObjectModel mergedModelFromBundles:nil
                                                                    forStoreMetadata:sourceMetadata];
    //Perform the migration...
    if (sourceModel) {
        NSMigrationManager *standardMigrationManager = [[NSMigrationManager alloc] initWithSourceModel:sourceModel
                                                                                      destinationModel:destinationModel];
        //Retrieve the appropriate mapping model...
        NSMappingModel *mappingModel = [NSMappingModel mappingModelFromBundles:nil
                                                                forSourceModel:sourceModel
                                                              destinationModel:destinationModel];
        if (mappingModel) {
            NSError *error = nil;
            NSString *storeSourcePath = [[self applicationDocumentsDirectory] stringByAppendingPathComponent:@"Recipes.sqlite"];
            NSURL *storeSourceUrl = [NSURL fileURLWithPath: storeSourcePath];
            NSString *storeDestPath = [[self applicationDocumentsDirectory] stringByAppendingPathComponent:@"Recipes2.sqlite"];
            NSURL *storeDestUrl = [NSURL fileURLWithPath:storeDestPath];

            //Pass nil here because we don't want to use any of these options:
            //NSIgnorePersistentStoreVersioningOption, NSMigratePersistentStoresAutomaticallyOption, or NSInferMappingModelAutomaticallyOption
            NSDictionary *sourceStoreOptions = nil;
            NSDictionary *destinationStoreOptions = nil;

            migrationSuccess = [standardMigrationManager migrateStoreFromURL:storeSourceUrl
                                                                        type:NSSQLiteStoreType
                                                                     options:sourceStoreOptions
                                                            withMappingModel:mappingModel
                                                            toDestinationURL:storeDestUrl
                                                             destinationType:NSSQLiteStoreType
                                                          destinationOptions:destinationStoreOptions
                                                                       error:&error];
            NSLog(@"MIGRATION SUCCESSFUL? %@", (migrationSuccess==YES)?@"YES":@"NO");
        }
    }
    else {
        //TODO: Error to user...
        NSLog(@"checkForMigration FAIL - No Mapping Model found!");
        abort();
    }
    return migrationSuccess;
}
1
votes

After futher investigation I found out I was experiencing the same issue as mentioned here (Core Data migration fails for to-one relationship). Settings the minimum to 1 instead of no minimum in my relationship made Core Data use my custom mapping model. While I'm not sure I assume this is a bug in Core Data.
However, this required me to change the original (1.0) mapping model (which users were already using) too. The fix I came up with is to create a new mapping model between 1.0 and 2.0 called 1.5. The only thing that's different in 1.5 in comparison with 1.0 is the minimum of the relationship, which is in 1.5 set to 1. Now, I could let Core Data perform a lightweight migration from 1.0 to 1.5 and hereafter perform my own custom migration from 1.5 to 2.0. Although it may be a hacky solution, it works perfectly. I pasted the code that handles the migration below.

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

    NSURL *storeUrl = [NSURL fileURLWithPath:[[self applicationDocumentsDirectory] stringByAppendingPathComponent:@"Project.sqlite"]];
    NSError *error;

    NSDictionary *storeMetadata = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType URL:storeUrl error:&error];

    if (! [[self managedObjectModel] isConfiguration:nil compatibleWithStoreMetadata:storeMetadata]) {
        // The current store isn't compatible with the model and should be migrated, check the version identifier of the store

        NSManagedObjectModel *sourceManagedObjectModel = [NSManagedObjectModel mergedModelFromBundles:nil forStoreMetadata:storeMetadata];

        if ([[sourceManagedObjectModel versionIdentifiers] containsObject:@"Model_1_0"]) {
            // The current version of the store is 1.0, a manual migration to 1.5 is needed in order to migrate finally to 2.0

            NSURL *destinationModelURL = [[NSBundle mainBundle] URLForResource:@"Model.momd/Model_1_5" withExtension:@"mom"];
            NSManagedObjectModel *destinationManagedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:destinationModelURL];

            NSMigrationManager *migrationManager = [[NSMigrationManager alloc] initWithSourceModel:sourceManagedObjectModel destinationModel:destinationManagedObjectModel];
            NSMappingModel *inferredMappingModel = [NSMappingModel inferredMappingModelForSourceModel:sourceManagedObjectModel destinationModel:destinationManagedObjectModel error:&error];

            NSURL *destinationURL = [NSURL fileURLWithPath:[[self applicationDocumentsDirectory] stringByAppendingPathComponent:@"Migration.sqlite"]];

            [migrationManager migrateStoreFromURL:storeUrl
                                             type:NSSQLiteStoreType
                                          options:nil
                                 withMappingModel:inferredMappingModel
                                 toDestinationURL:destinationURL
                                  destinationType:NSSQLiteStoreType
                               destinationOptions:nil
                                            error:&error];
            if (error) {
                DLog(@"Failed to migrate store from URL %@ with mapping model %@ to destination URL %@ with error %@", storeUrl, inferredMappingModel, destinationURL, error);
            }

            [[NSFileManager defaultManager] removeItemAtURL:storeUrl error:&error];
            [[NSFileManager defaultManager] moveItemAtURL:destinationURL toURL:storeUrl error:&error];
        }
    }

    NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption: [NSNumber numberWithBool:YES],
                              NSInferMappingModelAutomaticallyOption: [NSNumber numberWithBool:NO] };
    persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];

    if(![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:options error:&error]) {
        /*Error for store creation should be handled in here*/
        DLog(@"%@", error);
    }

    return persistentStoreCoordinator;
}
0
votes

The mapping model is probably not sufficient to handle the migration. In this case the mapping model will not be loaded, even if it matches source and destination model.

Write a test for the migration. Redo the changes to your model step by step and test the migration. That way you will find the change that does prevent the mapping model from loading.

Lookout for: Renaming attributes or creating attributes without specifying default values. Changing attributes to being non optional.

In those cases you have to specify the behavior manually within the mapping model.

0
votes

Had the same issue. I deleted an entity and renamed the relationship fields accordingly. I first tried to used lightweight migration and therefore specified renaming IDs for the relationships. Due to an oversight I mixed up the fields used for the "Renaming ID" and the "Hash Modifier". Once corrected everything works as expected.