Metadata

Today’s project is another of those oddballs: I’m not really sure it will be interesting to anyone else, and I’m far from confident that I implemented it optimally, but it did solve a real-world problem that cropped up in one of my products. It’s lightweight roll-your-own replacement for NSUserDefaults. Normally this sort of thing would be superfluous, so let’s begin with the “why” before diving into the “how”.

Motivation

In general, one doesn’t want to have to solve the problem of keeping two distinct data stores synchronized. For instance, if you store a set of records in one file, and an index of those records in a second file, you’re setting yourself up for problems: What happens if one file is saved successfully, but the write to its brother fails? What happens if an index from one pair is matched to a data file from another? This is nasty stuff, and best avoided.

Inside my iKnowPeople app, I keep track of a small quantity of metadata: information about how my Core Data backing store has been configured. This metadata would fit nicely into the NSUserDefaults system, but for the synchronization issue discussed above: NSUserDefaults and Core Data use distinct persistent stores, so there’s an insufficient guarantee that data and metadata would move in sync when being backed up, restored, or copied from device to device. It might work, or it might not, and I don’t want to think about it.

My solution was to implement a simple NSUserDefaults-like module that used Core Data as its backing store; this would guarantee that data and metadata would always move together.

Declarations

Here’s the heart of the PersistentDictionary declaration:

@interface PersistentDictionary : NSObject
{
	NSManagedObjectContext*		moc;
	NSEntityDescription*		entity;
}

- (id)initWithEntityName:(NSString*)name inContext:(NSManagedObjectContext*)managedObjectContext;

- (BOOL)hasValueForKey:(NSString*)key;

- (BOOL)boolForKey:(NSString*)key;						// Defaults to NO
- (float)floatForKey:(NSString*)key;						// Defaults to 0.0
- (double)doubleForKey:(NSString*)key;						// Defaults to 0.0
- (NSInteger)integerForKey:(NSString*)key;					// Defaults to 0
- (NSString*)stringForKey:(NSString*)key;					// Defaults to @""

- (void)setBool:(BOOL)value forKey:(NSString*)key;
- (void)setFloat:(float)value forKey:(NSString*)key;
- (void)setDouble:(double)value forKey:(NSString*)key;
- (void)setInteger:(NSInteger)value forKey:(NSString*)key;
- (void)setString:(NSString*)value forKey:(NSString*)key;

@end

I also declare 3 extension methods:

@interface PersistentDictionary ()

- (NSArray*)fetchForKey:(NSString*)key;
- (void)deleteForKey:(NSString*)key;
- (void)insertValue:(NSString*)value forKey:(NSString*)key;

@end

Initializer

Most of the definitions involved you can work out for yourself, so I’ll just go over a few highlights. To begin with, the initializer is the only slightly tricky bit:

- (id)initWithEntityName:(NSString*)name inContext:(NSManagedObjectContext*)managedObjectContext
{
	if (self = [super init])
	{
		NSEntityDescription* e = nil;
		@try
		{
			e = [NSEntityDescription entityForName:name inManagedObjectContext:managedObjectContext];
		}
		@catch (NSException* ne) {}
		if (!e)
		{
			[self release];
			return nil;
		}
		
		moc = [managedObjectContext retain];
		entity = [e retain];
	}
	return self;
}

Note that this method must be invoked with an Entity name, and that you must have added an appropriate Entity to your data model. In this context, “appropriate” refers to an Entity with (indexed) key and (non-indexed) value non-optional String attributes. If you’d defined an Entity like this:
metadata

Then you would invoke the initializer like this:

metadata = [[PersistentDictionary alloc] initWithEntityName:@"Metadata" inContext:someMOC];

Extension Methods

The PersistentDictionary uses three “helper” extension methods to do almost all its work:

- (NSArray*)fetchForKey:(NSString*)key
{
	// Create a request (for edited Event)
	NSFetchRequest* request = [[[NSFetchRequest alloc] init] autorelease];
	request.entity = entity;
	
	// Add a predicate to the request
	request.predicate = [NSPredicate predicateWithFormat:@"key == %@",key];
	
	// Do a simple fetch (ignore errors)
	NSError* err;
	return [moc executeFetchRequest:request error:&err];
}


- (void)deleteForKey:(NSString*)key
{
	// Perform the delete(s)
	for (NSManagedObject* o in [self fetchForKey:key])
	{
		[moc deleteObject:o];
	}

	// Commit the change(s)
	NSError* err;
	if (![moc save:&err])
	{
		// Handle the error.
		NSLog(@"Unresolved error %@, %@", err, [err userInfo]);
	}
}


- (void)insertValue:(NSString*)value forKey:(NSString*)key
{
	// Can't associate values with nil keys
	if (!key) return;
	
	// Create the object
	NSManagedObject* o = [[[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:moc] autorelease];

	// Store the KV pair
	[o setValue:key forKey:@"key"];
	[o setValue:value forKey:@"value"];
	
	// Commit the changes
	NSError* err;
	if (![moc save:&err])
	{
		// Handle the error.
		NSLog(@"Unresolved error %@, %@", err, [err userInfo]);
	}
}

Gets and Sets

Data retrieval is straight-forward; the only catch is making sure that defaults work properly. This method is typical:

- (BOOL)boolForKey:(NSString*)key
{
	NSArray* a = [self fetchForKey:key];
	if (![a count])
	{
		// On an error/nonexistent pair, default to NO
		return NO;
	}
	
	// In principle, there could be several pairs (this would be a bug); grab the first one arbitrarily
	// Note that on non-integer (i.e., buggy) values, this also defaults to 0 (NO)
	return [[[a objectAtIndex:0] valueForKey:@"value"] integerValue] == YES;
}

Set methods are even simpler:

- (void)setBool:(BOOL)value forKey:(NSString*)key
{
	[self deleteForKey:key];
	[self insertValue:[NSString stringWithFormat:@"%d",value] forKey:key];
}

It might have been better to add “change” logic instead of the “delete”/”insert” pair, but I can’t see any practical benefit to it, and it would have been just one more thing to code, test, and possibly go wrong.

Code

You can download the complete header and implementation files, if you’d like to use them in your own projects.

Share and Enjoy:
  • Twitter
  • Facebook
  • Digg
  • Reddit
  • HackerNews
  • del.icio.us
  • Google Bookmarks
  • Slashdot
This entry was posted in iPhone. Bookmark the permalink.

Comments are closed.