CoreData with multiple stores: configuration woes

Aneel picture Aneel · Apr 19, 2012 · Viewed 9.2k times · Source

I have an iOS project with a large, preloaded database and a small user database (both CoreData SQLite stores). Previous questions have suggested using configurations to control which Entities are used with which store. I'm having trouble getting that to work. Here's what I've been trying...

- (NSManagedObjectModel *)managedObjectModel
{
    if (_managedObjectModel != nil) return _managedObjectModel;
    // set up the model for the preloaded data
    NSURL *itemURL = [[NSBundle mainBundle] URLForResource:@"FlagDB" withExtension:@"momd"];
    NSManagedObjectModel *itemModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:itemURL];
    // set up the model for the user data
    NSURL *userDataURL = [[NSBundle mainBundle] URLForResource:@"UserData" withExtension:@"momd"];
    NSManagedObjectModel *userDataModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:userDataURL];
    // merge the models
    _managedObjectModel = [NSManagedObjectModel modelByMergingModels:[NSArray arrayWithObjects:itemModel, userDataModel, nil]];
    // define configurations based on what was in each model
WRONG [_managedObjectModel setEntities:itemModel.entities forConfiguration:@"ItemData"];
WRONG [_managedObjectModel setEntities:userDataModel.entities forConfiguration:@"UserData"];
    return _managedObjectModel;
}

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (_persistentStoreCoordinator != nil) return _persistentStoreCoordinator;
    // preloaded data is inside the bundle
    NSURL *itemURL = [[[NSBundle mainBundle] bundleURL] URLByAppendingPathComponent:@"FlagDB.sqlite"];
    // user data is in the application directory
    NSURL *userDataURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"UserData.sqlite"];

    NSManagedObjectModel *mom = self.managedObjectModel;
    NSError *error = nil;
    NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];

    if (![psc addPersistentStoreWithType:NSSQLiteStoreType configuration:@"ItemData"  URL:itemURL options:nil error:&error])
    {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
    ...

This aborts with "The model used to open the store is incompatible with the one used to create the store". Checking the hashes in the model against the hashes in the store show that they're identical for the Entities that are in the ItemData configuration.

If I try doing a lightweight migration, like so:

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

   NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];
   if (![psc addPersistentStoreWithType:NSSQLiteStoreType configuration:@"ItemData"  URL:itemURL options:options error:&error])

It fails with 'NSInvalidArgumentException', reason: 'Model does not contain configuration 'ItemData'.' I assume that's because a new model is being created by the lightweight migration process, and it doesn't contain my configuration.

Based on some suggestions in other threads, I've tried doing a lightweight migration without the configuration, and then creating a new coordinator using the configuration. This sort of works, but it adds tables to my preloaded .sqlite file corresponding to the user data entities (which don't belong there), and creates both the preloaded data tables and the user data tables in the newly-created user data store. The end result is that fetches fail, seemingly because they're looking in the wrong store.

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

// make a temp persistent store coordinator to handle the migration
NSPersistentStoreCoordinator *tempPsc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];
// migrate the stores
if (![tempPsc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:itemURL options:migrationOptions error:&error])
{
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
}
if (![tempPsc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:userDataURL options:migrationOptions error:&error])
{
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
}

// make a permanent store coordinator
NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];

NSDictionary *readOnlyOptions = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSReadOnlyPersistentStoreOption, nil];
if (![psc addPersistentStoreWithType:NSSQLiteStoreType configuration:@"ItemData"  URL:itemURL options:readOnlyOptions error:&error])
{
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
}

/*if (![psc addPersistentStoreWithType:NSSQLiteStoreType configuration:@"UserData" URL:userDataURL options:nil error:&error])
 {
 NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
 abort();
 }*/

And then later...

    OSAppDelegate *delegate = [UIApplication sharedApplication].delegate;
    NSManagedObjectContext *context = delegate.managedObjectContext;
    // sanity check
    for (NSPersistentStore *store in context.persistentStoreCoordinator.persistentStores) {
        NSLog(@"store %@ -> %@", store.configurationName, store.URL);
        NSMutableArray *entityNames = [[NSMutableArray alloc] init];
        for (NSEntityDescription *entity in [context.persistentStoreCoordinator.managedObjectModel entitiesForConfiguration:store.configurationName]) {
            [entityNames addObject:entity.name];
        }
        NSLog(@"entities: %@", entityNames);
    }

    NSFetchRequest *categoryFetchRequest = [[NSFetchRequest alloc] init];
    categoryFetchRequest.entity = [NSEntityDescription entityForName:@"Category" inManagedObjectContext:context];
    categoryFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@", categoryName];
    NSError *error = nil;
    Category *category = [[delegate.managedObjectContext executeFetchRequest:categoryFetchRequest error:&error] lastObject];

This works fine, returning the appropriately named Category object, until I uncomment the addition of the second store. If I do that, the fetch result comes back empty. The diagnostic NSLog messages print exactly what I expect. Each store is associated with correct configuration, and each configuration has the appropriate entities.

Can anyone point me at source code for a working multiple store setup, or clue me in to what I'm doing wrong? Thanks in advance!


SOLVED: The crux of the problem was the two lines marked WRONG in the first code listing. I was attempting to create configurations programmatically, but that seems to be insufficient. If you query the ManagedObjectModel for configurations after doing this, you do indeed see the configurations in the list, and the correct entities are associated with those configurations. However, it seems that something else needs to be done to make the PersistentStoreCoordinator able to properly use those. Creating the configurations in Xcode makes them work.


FOLLOW UP: There's an extra snag. The solution of running a separate migration pass before setting up the final Persistent Store Coordinator works great... in the simulator. On an actual device, the permissions are stricter. If you try to do that migration, it fails because the store in the App bundle is read-only. The migration seems to be necessary unless you consolidate your models. If you have only one model, and the store in the App bundle is compatible with it, the migration is not necessary and access using configurations defined in Xcode works.

Another option might be to move data into the Documents directory before attempting the migration. I haven't verified that that approach works.

Answer

Sunny picture Sunny · Apr 21, 2012

Have you tried having both configurations defined in the same model (i.e. same momd)? You can do this easily by selecting "Editor->Add Configuration" while editing one of your data models. Drag entities for UserData and ItemData into the appropriate configuration. The configuration specified this way is what Core Data respects; it's not about the file/URL name. Once you've done the above, then simplify your _managedObjectModel above to look for the single momd file/URL whenever it is called.

Alternatively, if you do decide to keep two separate momd files, make sure you've actually defined your models in the Configurations named "UserData" and "ItemData" respectively in their model definition files.

My initial suggestion is to keep one model file. Unless there is a reason these configurations cannot reside in the same object model, it doesn't make sense to complicate things with multiple files. I think it would be quite difficult to finesse Core Data into doing what you are trying to do above. Try to simplify the modeling part of your code.