Core Data Metadata
Jonathon Mah on 2007-05-06 07:26:36
I'm writing a time tracking application. Incredibly, I could not find a single good one — by “good”, I mean it needs to:
- be unobtrusive (have a decent UI)
- be crash-resistant — that is, it should save periodically, so if the system goes down data isn't lost
- allow manual entry — I often forget to start the timer before starting work, so it should be easy to go and add periods manually
- have rounding — I'm not reporting 1:32, but ideally it should track those extra minutes and round things out when I've accumulated enough “spare time”
- preferably be free. :)
OK, so I'll just write one myself. I'll store the work periods (“slogs”) with Core Data — it provides undo and huge scalability. The data model just has one entity. Different clients will be managed by different documents. There are several “document globals” (well, currently exactly one: the client name). It felt natural to keep these in the store metadata, a place that is global to the store (document). Apple's NSPersistentDocument tutorial describes how to save metadata, but not how to read it. It took some time to work it out.
Reading and writing metadata revolves around three methods of NSPersistentDocument:
(I'll call them read, write, and configure.) The issues appear to arise because store metadata is an attribute of the physical store, not its representation — you can only read and write metadata from a file, you can't do it in-memory. It would be nice to write metadata in write, and read it back in read. Alas. The first time a document is saved, write is called, but you can't set the metadata because the file doesn't exist on disk yet. You could call super's save, write the metadata and call save again, but that's ugly and potentially slow. However, on the first save configure is called once the new store has been set up. So you can write the metadata there. When saving subsequent times, configure is not called, so you need to write the metadata out in save.
OK, now all that's left is to read it in. The obvious (and correct) solution is to put your reading code in read. But it turns out that NSPersistentDocument calls configure in read. So you have to add an extra guard to the write in configure. Putting it all together:
Update 2007-05-07 02:24:56: I've found an error in the code originally posted, and I think it's a design flaw in Core Data. When writing an atomic store (XML or binary), the system will write “safely” by first writing to a temporary location. You can't get a store for that location in save (because it doesn't exist yet). The solution is to ask for the store at the original URL, set metadata on that, and save to the new URL. This will not change the metadata on the original store, despite what the code says. A similar situation occurs when saving as, for all store types. You have to set metadata on the prior store, and then Core Data will write that to the new store.
// MyPersistentDocument.m // Add an ivar: // BOOL shouldWriteMetadataDuringConfigure; - (BOOL)configurePersistentStoreCoordinatorForURL:(NSURL *)url ofType:(NSString *)fileType error:(NSError **)outError { BOOL success = [super configurePersistentStoreCoordinatorForURL:url ofType:fileType error:outError]; if (success && shouldWriteMetadataDuringConfigure) { NSPersistentStoreCoordinator *psc = [[self managedObjectContext] persistentStoreCoordinator]; id store = [psc persistentStoreForURL:url]; NSDictionary *metadata = [NSDictionary dictionaryWithObject:[self documentGlobals] forKey:@"HYDocumentGlobals"]; [psc setMetadata:metadata forPersistentStore:store]; shouldWriteMetadataDuringConfigure = NO; } return success; } - (BOOL)readFromURL:(NSURL *)url ofType:(NSString *)typeName error:(NSError **)outError { shouldWriteMetadataDuringConfigure = NO; BOOL success = [super readFromURL:url ofType:typeName error:outError]; if (success) { NSPersistentStoreCoordinator *psc = [[self managedObjectContext] persistentStoreCoordinator]; id store = [psc persistentStoreForURL:url]; NSDictionary *metadata = [psc metadataForPersistentStore:store]; [self setDocumentGlobals:[metadata valueForKey:@"HYDocumentGlobals"]]; } return success; } - (BOOL)writeToURL:(NSURL *)url ofType:(NSString *)typeName forSaveOperation:(NSSaveOperationType)saveOperation originalContentsURL:(NSURL *)originalContentsURL error:(NSError **)outError { // See if the persistent store is already set up (so we're able to write metadata to it) if (originalContentsURL) { NSPersistentStoreCoordinator *psc = [[self managedObjectContext] persistentStoreCoordinator]; id store = [psc persistentStoreForURL:originalContentsURL]; if (store) { NSDictionary *metadata = [NSDictionary dictionaryWithObject:[self documentGlobals] forKey:@"HYDocumentGlobals"]; [psc setMetadata:metadata forPersistentStore:store]; } else shouldWriteMetadataDuringConfigure = YES; } else // Persistent store hasn't been created yet; write the metadata while configuring it shouldWriteMetadataDuringConfigure = YES; return [super writeToURL:url ofType:typeName forSaveOperation:saveOperation originalContentsURL:originalContentsURL error:outError]; }

to search page content.