Update: The information below is for the original KSExtensibleManagedObject class and is now a smidge out of date.
The new version will break code-compatibility for existing uses, but not model compatibility. To fix, all you need do is override
-usesExtensiblePropertiesForUndefinedKey: in your subclass to return
YES when desired. This bridges the gap between key-value coding and the extensible properties 'silo'.
One of the limiting factors in the Core Data framework is that once you start shipping a particular version of your data model, it's rather fixed. Discover you need to add an extra attribute to an entity? Well I'm afraid that means writing code to handle migrating all the existing documents out there to your new model. Not very convenient if this is supposed to be just a minor bug fixing release.
A fairly simple solution is to include some general, spare attributes in each entity that can be used later on if needs be. It works, but isn't very neat; those extra key paths are not self-explanatory, each property is of a fixed type, and what if you need more of them?
Furthermore, in an application like Sandvox we need to provide persistent storage to a broad range of plugins, each with its own differing requirements. In the early days, Terrence experimented with taking each plugin's storage needs and merging them together to create a giant model. But of course, this is fragile, because as soon as you add in a new plugin type the whole model goes under.
So, I proudly present our final (or at least current) solution: KSExtensibleManagedObject. This has been written by myself as a new class that pulls together everything from our previous code, ready for the next Sandvox release.
On the surface KSExtensibleManagedObject operates just like a normal managed object. But, if you call
-setValue:forKey: it will happily return/store the value, rather than raise an exception as NSManagedObject would. For example, to support an extra property in your model named "foobar," without having to change the actual model in any way, you can just start calling
[setValue:x forKey:@"foobar"] without worry.
How is this magic accomplished? All you have to do is provide one extra key in your model of the type "binary data." Whenever a key is accessed that the model does not natively handle, KSExtensibleManagedObject steps in to convert and store it as raw data. By default, the attribute "extensiblePropertiesData" is used, but subclasses can override this if they wish. Here's a quick screenshot of how this is set up in the Sandvox data model:
The only other thing that KSExtensibleManagedObject asks of you is to pass it objects that conform to the NSCoding protocol in order for them to be archived (although there are methods provided for you to override that allow archiving using different means if desired).
There are of course a few limitations to the class. Foremost, just like transient Core Data properties, you cannot use any of the extensible properties in fetch requests since they are not directly available in the datastore. And of course while performance is good, it's not on a par with a native Core Data attribute. Other than that, everything should behave as is normal, including full undo/redo support with appropriate KVO notifications. Also, as Dan points out, Leopard is not required; KSExtensibleManagedObject works just fine on 10.4 and later.
So what are you waiting for? Download the class files now and start playing with it! I hope somebody else out there will find this code handy; certainly KSExtensibleManagedObject is being utilised for all the major entities of the Sandvox data model to provide us with greater flexibility.
Updated 19th April 2009: Minor bug fix (2 if statements needed to be swapped) and better API for querying changed/committed values.