Cocoa network persistence
Persisting NSDictionary ‘documents’ into the multi–user CouchDB store across the network
Apache CouchDB is a distributed, fault-tolerant and schema-free document-oriented database. CouchDB does support data structures within the document but deep hierarchies are best served using a relational database.
Document elements are stored in a JSON representation within a document. There is a natural “impedance match” between JSON and Cocoa’s Property List objects with the exception of NSDate. Dates will need to be serialised into strings.
Collections of documents are grouped in Views, specified by Map Reduce functions written in Javascript.
CouchDB has a low barrier to entry. CouchDB’s most beneficial feature to Watershed’s situation now is the casual integration with all categories of applications and the ease with which sophisticated shared storage is possible from application resource–sensitive client environments such as Cocoa on the iPhone and Javascript applications.
NSMutableDictionary Category
Persistence is achieved in Cocoa by using NSMutableDictionary as the container for each document. Each document can be identified by the identifer and the revision, stored in the dictionary using the NSString keys _id and _rev. The documents are mutable to track revisions over time. The persistence is implemented using a category on NSMutableDictionary. We haven’t done much work here—just assembled the pieces Lego style. Most of the hard work is done by json-framework which will need to be included in the project.
The category, composing of two methods, is partially listed below:
#define COUCHDB_ID_KEY @"_id"#define COUCHDB_REV_KEY @"_rev"#define HTTP_METHOD_POST @"POST"#define HTTP_METHOD_PUT @"PUT"@implementation NSMutableDictionary (NSDictionary_FBTFCouchDB)+ (id)dictionaryFromDocumentStore:(NSURL *)storeURL withIdentifier:(NSString *)identifer {if (!storeURL || !identifer) return nil;NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:identifer relativeToURL:storeURL] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:30.0];NSError *error = nil;NSURLResponse *response = nil;NSData *responseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];if (error) {NSLog(@"Network Connection Error: %@", [error localizedDescription]);return nil;}// Couch uses UTF8 encoding for transfersNSString *jsonString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];if (!jsonString) return nil;error = nil;SBJSON *jsonParser = [[SBJSON alloc] init];id result = [jsonParser objectWithString:jsonString allowScalar:YES error:&error];[jsonString release];[jsonParser release];if (error) {NSLog(@"JSON Parse Error: %@", [error localizedDescription]);return nil;}return result;}- (BOOL)persistInDocumentStore:(NSURL *)storeURL {// We do both inserts and updates in this methodNSString *methodName;NSURL *requestURL;if ([self objectForKey:COUCHDB_ID_KEY]) {// this dictionary originated from the document database so update by including ID in URLmethodName = HTTP_METHOD_PUT;requestURL = [NSURL URLWithString:[self objectForKey:COUCHDB_ID_KEY] relativeToURL:storeURL];} else {// This is a new Dictionary so we are insertingmethodName = HTTP_METHOD_POST;requestURL = storeURL;}NSMutableURLRequest *aRequest = [[NSMutableURLRequest alloc] initWithURL:requestURL];[aRequest setHTTPMethod:methodName];NSError *error = nil;SBJSON *jsonParser = [[SBJSON alloc] init];NSString *argsData = [jsonParser stringWithObject:self allowScalar:NO error:&error];if (error) {NSLog(@"JSON Serialisation Error: %@", [error localizedDescription]);return NO;}// Finish setting up the request and send to document store[aRequest setHTTPBody: [argsData dataUsingEncoding:NSUTF8StringEncoding]];NSURLResponse *response = nil;error = nil;NSData *responseData = [NSURLConnection sendSynchronousRequest:aRequest returningResponse:&response error:&error];[aRequest release];// Now parse the resultsNSString *result = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];error = nil;NSDictionary *results = [jsonParser objectWithString:result allowScalar:NO error:&error];if (error || [results objectForKey:@"error"]) {// Something went wrong. report, clean up and returnif (error) {NSLog(@"Can not parse results: %@", [error localizedDescription]);} else {NSLog(@"Database Error: %@", [results valueForKey:@"reason"]);}[jsonParser release];[result release];return NO;} else {// update the revision and the id (in case this is an insertion we do both)[self setValue:[results valueForKey:@"id"] forKey:COUCHDB_ID_KEY];[self setValue:[results valueForKey:@"rev"] forKey:COUCHDB_REV_KEY];[jsonParser release];[result release];return YES;}}@end
Other Possibilities & Further Development
The next step will be creating a database co–ordinator or connection proxy to manage persistence in a more sophisticated fashion. This can support features such as authentication & authorisation, revisions, rollback & merging of documents and the efficient storage & retrieval of collections.
Whilst CouchDB is not an object database and doesn’t encourage deep hierarchies, we’ve experimented recently combining CouchDB documents with Core Data’s NSManagedObjects by keeping document identifiers in managed objects and merging at display time. In the other direction, NSManagedObjects’ snapshots can easily be included in documents in the store when the NSManagedObject is saved in the context.
CouchDB is a peer-based distributed database system, it allows for users and servers to access and update the same shared data while disconnected and then bi-directionally replicate those changes later. We’d like to look into the possibility of using Couch, embedded in the desktop application bundle, locally and then allow CouchDB to replicate and sync between different instances as they appear on the network.