title | author | category | excerpt | retired | status | ||||
---|---|---|---|---|---|---|---|---|---|
NSIncrementalStore |
Mattt |
Cocoa |
Even for a blog dedicated to obscure APIs, `NSIncrementalStore` sets a new standard. It was introduced in iOS 5, with no more fanfare than the requisite entry in the SDK changelog. Ironically, it is arguably the most important thing to come out of iOS 5, which will completely change the way we build apps from here on out. |
true |
|
Even for a blog dedicated to obscure APIs, NSIncrementalStore
brings a new meaning to the word "obscure".
It was introduced in iOS 5, with no more fanfare than the requisite entry in the SDK changelog.
Its programming guide weighs in at a paltry 82 words, making it the shortest by an order of magnitude.
If it weren't for an offhand remark during WWDC 2011 Session 303, it may have gone completely unnoticed.
And yet, NSIncrementalStore
is arguably the most important thing to come out of iOS 5.
NSIncrementalStore
is an abstract subclass of NSPersistentStore
designed to "create persistent stores which load and save data incrementally, allowing for the management of large and/or shared datasets". And while that may not sound like much, consider that nearly all of the database adapters we rely on load incrementally from large, shared data stores. What we have here is a goddamned miracle.
For those of you not as well-versed in Core Data, here's some background:
Core Data is Apple's framework for object relational mapping. It's used in at least half of all of the first-party apps on Mac and iOS, as well as thousands of other third-party apps. Core Data is complex, but that's because it solves complex problems, covering a decades-worth of one-offs and edge cases.
This is all to say that Core Data is something you should probably use in your application.
Persistent stores in Core Data are comparable to database adapters in other ORMs, such as Active Record. They respond to changes within managed object contexts and handle fetch requests by reading and writing data to some persistence layer. For most applications, that persistent layer has been a local SQLite database.
With NSIncrementalStore
, developers now have a sanctioned, reasonable means to create a store that connects to whatever underlying backend you like--and rather simply, too. All it takes is to implement a few required methods:
NSPersistentStore
instances are not created directly. Instead, they follow a factory pattern similar to NSURLProtocol
or NSValueTransformer
, in that they register their classes with the NSPersistentStoreCoordinator
, which then constructs persistent store instances as necessary when -addPersistentStoreWithType:configuration:URL:options:error:
is called. The registered persistent store classes are identified by a unique "store type" string (NSStringFromClass
is sufficient, but you could be pedantic by specifying a string that follows the convention of ending in -Type
, like NSSQLiteStoreType
).
+initialize
is automatically called the first time a class is loaded, so this is a good place to register with NSPersistentStoreCoordinator
:
class CustomIncrementalStore: NSIncrementalStore {
override class func initialize() {
NSPersistentStoreCoordinator.registerStoreClass(self, forStoreType:self.type)
}
class var type: String {
return NSStringFromClass(self)
}
}
+ (void)initialize {
[NSPersistentStoreCoordinator registerStoreClass:self forStoreType:[self type]];
}
+ (NSString *)type {
return NSStringFromClass(self);
}
loadMetadata:
is where the incremental store has a chance to configure itself. There is, however, a bit of Kabuki theater boilerplate that's necessary to get everything set up. Specifically, you need to set a UUID for the store, as well as the store type. Here's what that looks like:
override func loadMetadata(error: NSErrorPointer) -> Bool {
self.metadata = [
NSStoreUUIDKey: NSProcessInfo().globallyUniqueString,
NSStoreTypeKey: self.dynamicType.type
]
return true
}
NSMutableDictionary *mutableMetadata = [NSMutableDictionary dictionary];
[mutableMetadata setValue:[[NSProcessInfo processInfo] globallyUniqueString] forKey:NSStoreUUIDKey];
[mutableMetadata setValue:[[self class] type] forKey:NSStoreTypeKey];
[self setMetadata:mutableMetadata];
Here's where things get interesting, from an implementation standpoint. (And where it all goes to hell, from an API design standpoint)
executeRequest:withContext:error:
passes an NSPersistentStoreRequest
, an NSManagedObjectContext
and an NSError
pointer.
NSPersistentStoreRequest
's role here is as a sort of abstract subclass. The request parameter will either be of type NSFetchRequestType
or an NSSaveRequestType
. If it has a fetch request type, the request parameter will actually be an instance of NSFetchRequest
, which is a subclass of NSPersistentStoreRequest
. Likewise, if it has a save request type, it will be an instance of NSSaveChangesRequest
(this article was originally mistaken by stating that there was no such a class).
This method requires very specific and very different return values depending on the request parameter (and the resultType
, if it's an NSFetchRequest
). The only way to explain it is to run through all of the possibilities:
- Result Type:
NSManagedObjectResultType
,NSManagedObjectIDResultType
, orNSDictionaryResultType
Return:
NSArray
of objects matching request
- Result Type:
NSCountResultType
Return:
NSNumber
NSArray
containing oneNSNumber
of count of objects matching request
Return: Empty
NSArray
So, one method to do all read and write operations with a persistence backend. At least all of the heavy lifting goes to the same place, right?
This method is called when an object faults, or has its values refreshed by the managed object context.
It returns an NSIncrementalStoreNode
, which is a container for the ID and current values for a particular managed object. The node should include all of the attributes, as well as the managed object IDs of any to-one relationships. There is also a version
property of the node that can be used to determine the current state of an object, but this may not be applicable to all storage implementations.
If an object with the specified objectID
cannot be found, this method should return nil
.
This one is called when a relationship needs to be refreshed, either from a fault or by the managed object context.
Unlike the previous method, the return value will be just the current value for a single relationship. The expected return type depends on the nature of the relationship:
- to-one:
NSManagedObjectID
- to-many:
NSSet
orNSOrderedSet
- non-existent:
nil
Finally, this method is called before executeRequest:withContext:error:
with a save request, where permanent IDs should be assigned to newly-inserted objects. As you might expect, the array of permanent IDs should match up with the array of objects passed into this method.
This usually corresponds with a write to the persistence layer, such as an INSERT
statement in SQL. If, for example, the row corresponding to the object had an auto-incrementing id
column, you could generate an objectID with:
self.newObjectIDForEntity(entity, referenceObject: rowID)
[self newObjectIDForEntity:entity referenceObject:[NSNumber numberWithUnsignedInteger:rowID]];
Going through all of the necessary methods to override in an NSIncrementalStore
subclass, you may have found your mind racing with ideas about how you might implement a SQL or NoSQL store, or maybe something new altogether.
What makes NSIncrementalStore
so exciting is that you can build a store on your favorite technology, and drop that into any existing Core Data stack with little to no additional configuration.
Even though NSIncrementalStore
has been around since iOS 5, we're still a long way from even beginning to realize its full potential. The future is insanely bright, so you best don your aviators, grab an iced latte and start coding something amazing.