Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Core Data - lightweight migrations and multiple core data model files (xcdatamodel)

I'm having a problem performing a lightweight migration when migrating from a store that is defined by two separate xcdatamodel files.

In version 1.0 of my app, I had the models broken out into an analytics model, model-A, and everything else in model-B. When compiling, the models would be grouped together and everything proceeded smoothly.

When working on the new version, 1.1, I upgraded model-B by adding a new model version to model-B and setting that new version as active.

The issue arises when upgrading from 1.0 to 1.1. It seems Core Data checks the model store on disk (created by version 1.0) and looks for the model that describes it but is unable to find a SINGLE model that defines the entire store (model-A only covers analytics, and model-B covers everything else), so it throws a "Can’t find model for source store" error.

Has anyone found a solution for separating out models but still allowing upgrades + lightweight migrations to work without the extra hassle of defining custom migrations?

Here is the snippet of code used to load models:

    NSArray *modelNames = [NSArray arrayWithObjects:@"model-A", @"model-B", nil];
    NSMutableArray *models = [NSMutableArray array];
    for (NSString *name in modelNames)
    {
        LogInfo(@"loading model %@", name);
        NSURL *modelURL = [[NSBundle mainBundle] URLForResource:name withExtension:@"momd"];
        NSManagedObjectModel *model = [[[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL] autorelease];
        [models addObject:model];
    }

    // combine all the separate models into one big one
    objectModel = [[NSManagedObjectModel modelByMergingModels:models] retain];

    NSURL *documentsDirectory = [NSURL fileURLWithPath:[SuperFileManager documentsDirectory] isDirectory:YES];
    NSURL *storeURL = [documentsDirectory URLByAppendingPathComponent:@"database.sqlite"];
    NSError *error = nil;

    coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:objectModel];
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                                                  [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
                                                  [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,
                                                  nil];

    if (![coordinator addPersistentStoreWithType:NSSQLiteStoreType
                                   configuration:nil
                                             URL:storeURL
                                         options:options
                                           error:&error])
    {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
like image 913
Mark Avatar asked Jun 07 '12 22:06

Mark


3 Answers

After attending a WWDC 2012 lab and meeting with the Core Data team, it seems you are forced to put all your model info in a single xcdatamodel. CoreData is not intelligent enough to check its existing stores as a combination of the stores that created it and are still on disk. As C. Roald pointed out, you can do some processing on old xcdatamodel files, but it's quite sad that Core Data does not handle this more elegantly.

like image 84
Mark Avatar answered Nov 10 '22 00:11

Mark


I encountered this problem also. I lost several hours trying to figure out WTF -- very frustrating.

I believe the easiest way to solve this problem is:

  1. Pick which model you're keeping -- say ModelB -- and create a new version for it based on the published version. I'll call the published version ModelBv1 and the new version ModelBv1_merge.

  2. Open contents XML files for ModelAv1 and ModelBv1_merge in a text editor (ie, ModelA.xcdatamodeld/ModelAv1.xcdatamodel/contents and ModelB.xcdatamodeld/ModelBv1_merge.xcdatamodel/contents) and merge the XML by hand. The schema is very simple -- just copy the <entity> elements and merge the <elements> element (into the _merge contents file) and you're done.

  3. Open the contents file for your new ModelBv2 and again merge ModelA contents into it.

  4. Remove ModelA from your project file.

Check in Xcode that ModelBv1_merge and ModelBv2 look sane, and contain everything you expect (the union of old Model A and Model B). Build and you should be done.

(I think this has a caveat of "provided both contents files were written by the same version of Xcode", but I think if you have an old contents file it should be easy enough to make Xcode rewrite it by making a trivial change somewhere.)

like image 26
c roald Avatar answered Nov 10 '22 02:11

c roald


I have a scenario in which my application model is obtained merging multiple models, and I managed to have a kind of automatic lightweight migration in this way:

NSError* error = nil;
NSURL *documentsDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
NSURL *storeURL = [documentsDirectory URLByAppendingPathComponent:@"db.sqlite"];
NSString* storePath = [storeURL path];
NSLog(@"Store URL: %@", storeURL);
if( [[NSFileManager defaultManager] fileExistsAtPath:storePath] ){
    // Load store metadata (this will contain information about the versions of the models this store was created with)
    NSDictionary *storeMeta = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:nil URL:storeURL error:&error];
    if(storeMeta){
        // Get the current model, merging all the models in the main bundle (in their current version)
        NSManagedObjectModel* model=[NSManagedObjectModel mergedModelFromBundles:nil];
        // If the persistent store is not compatible with such a model (i.e. it was created with a model obtained merging old versions of "submodels"), migrate
        if(![model isConfiguration:nil compatibleWithStoreMetadata:storeMeta]){


            // Load the old model
            NSManagedObjectModel*oldModel = [NSManagedObjectModel mergedModelFromBundles:nil forStoreMetadata:storeMeta];

            // Compute the mapping between old model and new model
            NSMappingModel* mapping = [NSMappingModel inferredMappingModelForSourceModel:oldModel destinationModel:model error:&error];
            if(mapping){
                // Backup old store
                NSURL* storeBackupURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:[NSString stringWithFormat:@"db.sqlite.%@.bck", [NSDate new]]];
                BOOL done = [[NSFileManager defaultManager] moveItemAtURL:storeURL toURL:storeBackupURL error:&error];
                if(done){
                    // Apply the mapping
                    NSMigrationManager* migrationManager = [[NSMigrationManager alloc] initWithSourceModel:oldModel destinationModel:model];
                    BOOL done = [migrationManager migrateStoreFromURL: storeBackupURL
                                                                 type: NSSQLiteStoreType
                                                              options: nil
                                                     withMappingModel: mapping
                                                     toDestinationURL: storeURL
                                                      destinationType: NSSQLiteStoreType
                                                   destinationOptions: nil
                                                                error: &error];
                    if(done){
                        NSLog(@"Store migration successful!!!");
                    }
                }
            }
        }
    }
}

if(error){
    NSLog(@"Migration error: %@", error);
}
like image 28
Mirko Luchi Avatar answered Nov 10 '22 00:11

Mirko Luchi