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

4

16

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();
    }
Mitten answered 7/6, 2012 at 22:7 Comment(0)
M
9

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.

Mitten answered 13/12, 2012 at 2:41 Comment(1)
@codingrogue - unfortunately not. The team I was working on at the time abandoned using Core Data and I switched over to Android programming. Sorry. :/Mitten
R
8

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.)

Rene answered 7/12, 2012 at 0:11 Comment(2)
Hacktastic way to still keep have 2 data model files (even if only temporarily! Thanks! :)Mitten
Hello, this way is working great at simulator but it is not working at device??Lexicologist
G
5

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);
}
Gabion answered 10/6, 2014 at 15:43 Comment(5)
I just found myself writing pretty much this same code before I read your reply. Although ugly, this seems like the only way to do this.Mohamed
Wow, thanks, I have been stuck with this problem for hours. Just one question, do you keep all the backup files? I'm guessing once the migration is done there's no harm in deleting thoseFasciation
Wow this really saved my ass. Thanks a bunch!! My specific issue was that we hade one old xcdatamodeld for manual migration from a legacy app, and a proper versioned new xcdatamodeld. I guess Core Data was choking on this.Sullage
Did you ever try this on iOS 7 as well? It doesn't work there :(Sullage
Note that this is automatic but not lightweight. So for a large store it could take a long time or possibly run out of RAM.Corrade
O
0

The best way to upgrade your Core Data model is to add a version. If you don't do that, you will perish in crashes, update perils and stuff like that.

Adding a new version is actually quite easy. You select the datamodel file and select 'Editor > Add model version'. This will enable you to add a new database version based on the previous model. You then need to set the current datamodel to the latest: http://cl.ly/2h1g301b0N143t0b1k2K

iOs will migrate the data automatically (at least in my case) when a new version is installed.

hope this helps.

Orcinol answered 18/6, 2012 at 15:22 Comment(1)
This works fine when you have a single xcdatamodel. If you have multiple and you upgrade one of them, Core Data can't reconcile things. After attending a WWDC 2012 lab, it seems like you're basically forced to put all your model info in a single xcdatamodel and do as you suggest!Mitten

© 2022 - 2024 — McMap. All rights reserved.