5

PROBLEM STATEMENT

When attempting to save a record to a read/write store that is one of two SQLite stores assigned to the same PersistentStoreCoordinator, my iPhone app crashes. One obvious issue when saving a record is the PersistentStoreCoordinator does not know in which Store to save the data (only because I don't know how to make this happen).

First I will provide the big picture to make sure my approach is sound. Then I will supply the implementation details.

BACKGROUND

This is a simplified example representing the key aspects of the actual app I am working on.

Managed Object Model

Seed Data

Seed Data

User Input Scenario

User Input Scenario

CURRENT IMPLEMENTATION

Core Data Implementation

Core Data Implementation

Data Storage and Retrieval

Data Storage and Retrieval

Of course, there should be no evidence to the user, when looking at pick lists to make a choice for an attribute, that the choices come from two different Stores.

Persistent Store Coordinator Setup

- (NSPersistentStoreCoordinator*)persistentStoreCoordinator {
   if (_persistentStoreCoordinator == nil) {
       NSArray *bundles = @[[NSBundle bundleForClass:[self class]]];
       _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[NSManagedObjectModel mergedModelFromBundles:bundles]];

       NSError *error;
       //--------------------------------------------------
       // Set options for the USER DATA Persistent Store.
       NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption : @YES,
                                       NSInferMappingModelAutomaticallyOption : @YES};
       //--------------------------------------------------
       // Add the USER DATA Store to the Persistent Store Coordinator.
       NSPersistentStore *persistentStore = [_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
                                                                                   configuration:nil
                                                                                             URL:self.persistentStorePathForUserData
                                                                                         options:options
                                                                                           error:&error];
       //--------------------------------------------------
       // Set options for the SEED DATA Persistent Store.
       options = @{NSMigratePersistentStoresAutomaticallyOption : @YES,
                         NSInferMappingModelAutomaticallyOption : @YES,
                                NSReadOnlyPersistentStoreOption : @YES};
       //--------------------------------------------------
       // Add the SEED DATA Store to the Persistent Store Coordinator.
       persistentStore = [_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
                                                                configuration:nil
                                                                          URL:self.persistentStorePathForSeedData
                                                                      options:options
                                                                        error:&error];
   }
   return _persistentStoreCoordinator;
}

IMPORTANT GOALS

Keep in mind the following:

  1. I would prefer, if at all possible, to do seed data updates without having to manage data versions behind the scenes (i.e. supplying with app updates only Seed Data records that are new or changed and somehow handling deletions) or implementing version-checking functionality in the code to handle a situation in which a user is upgrading from version n to n+5.
  2. User Data and Seed Data should not contain any duplicate records between the two and use the same ManagedObjectModel. So from data and model perspectives, there should be no need to merge the two stores or migrate one store to another.

RESEARCH

In this case, Save objects from multiple stores to single persistent store, the two Stores were merged followed by fetching all the records, culling out the duplicates, and saving the context. I'm hoping not to have to merge and then check thousands of records for duplicates. (See Important Goal #2 above.)

In this case, What is an efficient way to Merge two iOS Core Data Persistent Stores?, certain entities are read-only and others are read/write. In my app, all entities in the Seed Data Store are read-only and the same entities in the User Data Store are read/write. So I don't think migration is applicable. (See Important Goal #2 above.)

In Apple's, Core Data Programming Guide under "Persistent Store Coordinator", Figure 4 "Advanced persistence stack" shows a Core Data implementation using two Stores, but again, each Store is configured with separate and distinct objects. In my app, each object appears in each Store.

The solution proposed here, Combining Two SQLite Stores Into One, relative to having two stores without relationships between the objects in the different stores seems relevant, but no details are provided to compare to what I have implemented.

I have gone through the first three chapters of, Core Data (2nd Edition), by Marcus Zarra, but he doesn't get into using two stores that don't require migration. Chapter 3, however, does provide a very clear example of versioning. (It's complexity lead me to Important Goal #1 above.)

This solution, Which persistent store is used by default in core data in iPhone, suggests using multiple configurations of the ManagedObjectModel, but each entity is assigned to one and only one configuration. I'm not sure how, or even if, this solution can be extrapolated to my situation.

Maybe the solution proposed here, NSPersistentStoreCoordinator with two types of persistent stores?, is close to what I need. Unfortunately, only requests are addressed. I don't see a method analogous to NSFetchRequest method, setAffectedStores, in Class NSManagedObjectContext for saves.

Community
  • 1
  • 1
AnrosApps
  • 131
  • 8

2 Answers2

2

Thanks to Ray Wenderlich's referral to Mic Pringle, Mic proposed a Managed Object Model architecture with which I was able to solve the issue while adhering to my goals. The key to the solution is utilizing an abstract entity as a parent entity to user and seed entities.

Managed Object Model

With this architecture, it is possible to create two Configurations that are assigned to separate stores: 1) UserData - r/w store located in the User's Documents directory.

UserData Configuration

2) SeedData - r only store located in the App Bundle.

SeedData Configuration

The downside is that record ID's must be maintained for the seed data entities (since relationships are not allowed between Configurations), but the huge upside is that changes or additions can be made to the seed data without affecting the user's entries AND without having to adopt any of the cumbersome solutions discussed in the RESEARCH section of the original post of this question.

Community
  • 1
  • 1
AnrosApps
  • 131
  • 8
  • Are you absolutely force to have `_Seed` entities? I would have to achieve the same with a parent entity ready-only and a rw child entity. – Rivera Sep 25 '14 at 02:38
  • The seed data is stored in the App Bundle and assigned to the SeedData Configuration. The user data is stored in the user's Documents directory and assigned to the UserData Configuration along with the parent entity. Relationships cannot exist between Configurations. If you wanted to keep seed data in the parent entity, then you would need to remove the relationship between the parent and user entities and replace the relationship with an ID in both the parent and user entities. That requires you manually having to deal with an ID in the user entity. – AnrosApps Sep 26 '14 at 11:26
  • Also, the parent entity is an abstract entity. An object cannot be created on an abstract entity. Even if it wasn't abstract, the need for a user ID would still exist. – AnrosApps Sep 26 '14 at 11:40
  • My question was it is really needed to have three entities, `XXX`, `XXX_Seed` and `XXX_User` or if just two could be enough. From my tests it seems that yes they are all needed as not only relationships can't be shared among stores, super entities also don't like to be on a different configuration. – Rivera Sep 29 '14 at 03:20
1

For the core question of saving data with one context/coordinator and two stores, the approach you want is:

  1. When adding the user-editable store via addPersistentStore, save a reference to the NSPersistentStore object that is returned.
  2. When creating a new object to save in the user-editable store, do something like this:

    NSManagedObject *newObject = [NSEntityDescription insertNewObjectForEntityForName:@"Vehicle" inManagedObjectContext:self.managedObjectContext];
    [self.managedObjectContext assignObject:newObject toPersistentStore:userEditableStore];
    

    The key here is to explicitly call assignObject:toPersistentStore: before saving changes.

For related questions:

I would prefer, if at all possible, to do seed data updates without having to manage data versions behind the scenes...

If you leave the non-editable store in the app bundle (i.e. you don't copy the file to somewhere else), you can just include a new version of the data with a new version of the app. You'll always be using whatever version is in the app bundle, so you'll have the latest data.

If you do end up copying data from the seed store to the user store, make sure that each entry includes the app (or seed store) version number when it was added. That'll make it easy to avoid duplicates.

User Data and Seed Data should not contain any duplicate records between the two and use the same ManagedObjectModel. So from data and model perspectives, there should be no need to merge the two stores or migrate one store to another.

You don't need to copy data from one to the other if you don't have any relationships between objects in different stores-- because that's just not permitted in Core Data. If you need relationships (and it sounds like you do) then look into fetched properties. These look a lot like an attribute or a relationship on an entity type, but internally they look up values from the persistent store(s). With multiple persistent store files this allows something that's almost but not quite like a relationship between objects in different stores.

Tom Harrington
  • 69,312
  • 10
  • 146
  • 170
  • Thanks, Tom! A few things before I try some of this out. – AnrosApps May 15 '14 at 13:09
  • First, I don't see any reason to copy the non-editable store in the app bundle to any other location. When a user updates their app, there simply will be a new version of the non-editable store in the bundle. I hope I'm not missing something. Also, I don't understand why I would need relationships between the two stores - probably due to my unfamiliarity with how this all works. – AnrosApps May 15 '14 at 13:18
  • I may not be understanding your data model completely. Basically, you can't have a relationship from an object in one persistent store to an object in a different persistent store. It sounded to me like you might need that (e.g. a relationship from a `TestDrive` in the user-editable data store to a `Vehicle` in the pre-load store) but maybe not. If you do, fetched properties are the way to go, but if not then just forget it. – Tom Harrington May 15 '14 at 15:59
  • I guess you are right. Any relationship that exists between entities in the same store must also be made between those entities that reside in opposite stores. This should only come into play when editing or adding a record that includes attributes from both stores. I'm first going to attack the issue with a record that only includes attributes from the same store. – AnrosApps May 15 '14 at 21:24
  • I have an NSManagedObject Class set up for each entity in the model. I can’t assign an instance of an entity in the NSManagedObject Class because I won’t know with which store the object is associated since it could be either. I tried making the store assignment in a Class in which I do data management. I figured if a record is being saved, then it has to be saved in the userEditableStore. – AnrosApps May 15 '14 at 21:25
  • `+(void)saveChanges:(NSError **)error {NSManagedObjectContext *moc = self.managedObjectContext; NSSet *objectsWithChanges = [moc updatedObjects]; for (NSManagedObject *mo in objectsWithChanges) { [moc assignObject:mo toPersistentStore:self.userEditableStore]; } [moc save:error]; }` – AnrosApps May 15 '14 at 21:25
  • I put a breakpoint in the “for” loop and did a “po mo” and “po self.userEditableStore”. Both look as I would expect them to. Upon executing assignObject:toPersistentStore:, I get an error that says, “Can’t reassign an object to a different store once it has been saved.” I tried editing a record that I know resides in the userEditableStore and commented out the “for” loop. The change is saved with no errors. If I try adding a record without the “for” loop, I get an assertion failure – the problem that initiated this issue to begin with. – AnrosApps May 15 '14 at 21:28
  • I’m not sure where else to try assigning an object to a specific store. – AnrosApps May 15 '14 at 21:30
  • You only assign objects to a store **immediately after you create them** but **before you save changes**. You never call that method at any other time on any other object, ever. – Tom Harrington May 15 '14 at 21:30