Thursday, January 17, 2013

A couple of notes about stores

A few posts ago I talked about how you can use Apple's core data in games by employing the techniques shown in the XCode Core-Data command line example.

That example works well because it doesn't assume any code templates - and if you're coding a game you're probably not using a core data template - and also it doesn't assume using utility classes like NSPersistentDocument.

However some of the file saving semantics of Core Data stores are pretty weird, so I thought I would document my current findings here.  The relationship between the NSPersistentStore object, and the on-disk file are also a bit obscure in the documentation.  So call this the "what they don't tell you in the Apple documentation" article.

Stores and Coordinators

All the three functions I discuss below are on NSPersistentStoreCoordinator.  I suggest never saving a reference to your store object.  The action of saving or moving a store will likely invalidate any instance of NSPersistentStore you have saved.  Instead do something like this:

- (NSPersistentStore *)store
{
    NSArray *stores = [[m_managedObjectContext persistentStoreCoordinator] persistentStores];
    
    NSAssert([stores count] <= 1, @"Each world can have (at most) one backing store!");
    return [stores lastObject];
}


In other words, use the coordinator as the place for all your store operations - not the instances of the stores themselves.  Weird - maybe but that is how Cocoa does it.

Who ya Gonna Call?

The documentation says: Adds a new persistent store of a specified type at a given location, and returns the new store.

What the documentation doesn't make specifically clear is that this function will create the on-disk file, in this case an SQLite data store if required - but it will also just transparently open the existing on-disk file if there is one there.  Nice.

Here the relation with the store object is clear: it creates the on-disk file (or opens an existing one) and returns a new store object.  You can use the returned store object for checking success, but as mentioned above I suggest not saving a reference to it.

This is your go-to function for opening a data store file, existent or not, given its location.  In the simple case you know your type and can simply specify it - eg NSSQLiteStoreType, and you don't have any configuration or options.

There's no need to use NSFileManager's exists functions or anything like that, to check for existence of the file first.  Of course you will need to make sure that any intermediate directories in the path for the URL do exist.

If you want to do basic error checking just test the returned value for nil - there's no need to get the error argument unless you want verbose error reporting.  To understand how this works simply doing the two modes (initial creation versus subsequent re-opening) checkout the main.cpp of the XCode Core-Data command line example.  Might not seem like a big deal, but as these are databases which normally require a bunch of setup, its nice to have a one-stop-shop for opening like this.

The Mad and the Bad

The documentation says: Sets the URL for a given persistent store.

No shit, Sherlock!  This is one of the least useful functions, and most poorly documented, on NSPersistentStoreCoordinator.  Basically the intent of it seems to be a very cheap way to open already existing data store files, where you absolutely know your existing store object has the right setup, and you know for sure you have an existing store file.  The fact it has a BOOL return looks promising for light-weight error detection (but it turns out not to be).

Here you must already have a store object.  Even though these calls are on NSPersistentStoreCoordinator, which can make new store objects, this call is not going to do that - you must have an existing store.  What if you don't save before calling this?  Changes to the store might get lost - in some cases.  See the documentation but its got to do with whether or not you have an atomic store - basically SQLite is non-atomic.

The arguments are the URL to open, and an existing store object - that is an instance of NSPersistentStore.  But what beats me is in what circumstances would you have a valid instance of a store object, but either not have it opened to a backing store URL, or want to just cut that store file loose?

Another problem is you must have an existing backing file - which the documentation says.  What it doesn't say is what happens if you don't?  Well, the function quite happily returns true if the file doesn't exist - huh??? - and then when you try to call save on your NSManagedObjectContext at a later point you get an exception.  Joy.

If anyone can find a great use for this function tell me - because as far as I can tell its mad, bad and dangerous to know.

Saving As

The documentation says: Moves a persistent store to a new location, changing the storage type if necessary.

This call is basically a addPersistentStoreWithType:configuration:URL:options:error: call using your old stores type, followed by a removePersistentStore:error: call on the old store (assuming you don't change the store type).

What I mean by that is if you have a store saved at URL A and  you want to make a copy of it at URL B, and do subsequent saves and operations on that new URL B, then this call will do what you need.

Here you are going to lose the store you pass in - but if you follow the idiom above of not saving a reference to your stores that is no problem.  Rather than BOOL this returns a new store which you can check against nil if you want to determine success.

Obviously its also the go to function if you need to migrate between store types for some reason - though I'm not really sure why you'd want to do that so much.  In most applications you decide on say SQLite and stay with it.

Conclusions

  • Use addPersistentStoreWithType:configuration:URL:options:error: to open all your data files, and also to create them the first time.  Don't keep a member variable of the store it returns.
  • Once a file is open, you can save the data to it by calling save: on your NSManagedObjectContext.  That will cause the attached stores to save to whatever URL's they're opened onto.
  • To do a save as use migratePersistentStore:toURL:options:withType:error: and pass in your existing store object, obtained from the - (NSPersistentStore *)store function listed above.
  • Closing files is not necessary - just let the objects go out of scope and ARC plus their destructors will clean everything up.  Make sure you save first obviously.
  • I suggest hiding all references to stores inside a class, and doing everything through NSPersistentStoreCoordinator.
Farewell and may all your data persist!