4
votes

Starting from the standard Xcode Document-based Application w/ CoreData template on 10.7, I'm experiencing some frustrating behavior. I'm sure it's something simple that I'm overlooking.

Let's say in my NSPersistentDocument subclass, I have something like this, hooked up to a button in the window:

- (IBAction)doStuff:(id)sender
{        
    NSEntityDescription* ent = [[self.managedObjectModel entitiesByName] valueForKey: @"MyEntity"];
    NSManagedObject* foo = [[[NSManagedObject alloc] initWithEntity: ent insertIntoManagedObjectContext: self.managedObjectContext] autorelease];
    [self.managedObjectContext save: NULL];
}

If I create a new document and click that button, I'll get the following error: This NSPersistentStoreCoordinator has no persistent stores. It cannot perform a save operation. I get this. We haven't saved yet, there are no persistent stores. Makes sense.

Now let's say I split this out into two actions, hooked up to different buttons, like so:

- (IBAction)doStuff:(id)sender
{        
    NSEntityDescription* ent = [[self.managedObjectModel entitiesByName] valueForKey: @"MyEntity"];
    NSManagedObject* foo = [[[NSManagedObject alloc] initWithEntity: ent insertIntoManagedObjectContext: self.managedObjectContext] autorelease];
}

- (IBAction)doOtherStuff:(id)sender
{        
    [self.managedObjectContext save: NULL];
}

If I create a new document and press the first button, then at some indeterminate time after pressing that button (dirtying the document), autosave will come along and autosave the document, which will create a store in the temp location. If I then press the second button, there are no complaints (since there's now a store.)

I need my document to be able to do managedObjectContext saves from the get-go. I'm kicking off some stuff on a background thread, and I need the background context's save operation (and notification) in order to merge the changes made by the background thread into the main thread's managedObjectContext.

I thought about trying to force an autosave, but the autosave process appears completely asynchronous, so I'd have to jump through hoops to disable any UI interaction that could cause a managedObjectContext save until the first autosave operation completes.

I also considered creating an in-memory store to bridge the gap between creating a new document and the first autosave, but then it's not clear to me how I would migrate that in memory store to the disk store and remove the memory store synchronously with the first autosave operation.

Anyone have any ideas on how I might handle this?

4
Have you created a store in your initialisation?colincameron
No, I haven't, because I specifically want the temporary store (prior to user-initiated document save) to be wherever the autosave mechanism thinks it should be.ipmcc

4 Answers

7
votes

So I fooled around with this for a while, including trying @Aderstedt's suggestion. That approach didn't work, because faking a notification appears to merely tell the receiving context "hey, check with the persistent stores, I've updated them!", when in reality, I haven't, because there are none. I eventually found an approach that worked. Unfortunately it relies on Lion-only features, so I'm still looking for a way to do this that doesn't require Lion.

Background

I wanted to work with the NSPersistentDocument approach. Although I've not found this documented explicitly anywhere, I found several forum posts, and experienced a bunch of empirical evidence that you cannot call -[NSManagedObjectContext save:] on a context that belongs to an NSPersistentDocument. As mentioned in the question, if you call that before the document has ever been saved, it'll have no stores, so the save will fail. Even after the store exists, by saving the context directly (and not via the document save API) you are effectively changing the on-disk representation behind the NSPersistentDocument's back, and you will get the document popping sheets that say:

File has been modified by another application

In short, the NSPersistentDocument expects to control the saving action of the associated NSManagedObjectContext itself.

Also worth mentioning up front: the goal here was to make sure that the context used by the UI would trigger no (or at least minimal) I/O in order to remain responsive. The pattern I eventually settled on was to have 3 contexts. One context owned by the NSPersistentDocument, which would be responsible for doing file I/O in conjunction with the document. A second, effectively read-only, context for binding the UI to. (I realize that many folks want UI that mutates the model, so this may be less thrilling for them, but it wasn't a requirement for me.) And a third context for use on a background thread that asynchronously loads data from a web service, and hopefully pushes it into the other contexts so that it can be both saved on disk and presented in the UI without potentially blocking the UI on network I/O.

Lion-only Solution

The new parent/child NSManagedObjectContext feature in Lion's CoreData implementation is perfect for this. I replaced the NSPersistentDocument's NSManagedObjectContext with an new MOC of concurrency type NSPrivateQueueConcurrencyType. This will be the "root" context. Then I made the UI context with NSMainQueueConcurrencyType concurrency, and made it a child of the root context. Lastly I made the network-loading context a NSPrivateQueueConcurrencyType context which is a child of the UI context. The way this works is that we kick off a network load operation in the background, it updates the network context. When it's done, it saves the context. With parent/child relationships, saving a child context pushes the changes up into the parent context (the UI context) but does not save the parent context to the store. In my case, I also listen for the NSManagedObjectContextDidSaveNotification notification from the network context and then tell it's parent to save as well (which will push the changes from the UI context into the root/disk context, but will not save it to disk.)

At the end of this chain of events all the contexts are consistent, and we still haven't forced a real save of the underlying root context, and so we haven't run afoul of the NSPersistentDocument in its role of managing the on-disk representation.

One catch is that if you want to prevent saves of child contexts from generating undo (i.e. this was a network loading operation, there's nothing to undo) you have to disableUndoRegistration on each parent context as you propagate the changes up the chain.

Pre-Lion Efforts

I would really have liked to find a pre-Lion-compatible solution for this issue. I tried a few things before giving up. I first tried associating an in-memory store with the PSC on document init, so that I could do NSManagedObjectContext saves before the document saved, then migrate the in-memory store on the first save. That part worked great. But once an on-disk store existed, this approach was bogus because after it's saved to disk we have the same problem where any saving of MOCs connected to PSCs owned by an NSPersistentDocument have to be done by the document.

I also tried hacking up a mechanism to move changes from one context to another using the NSManagedObjectContextObjectsDidChangeNotification payload. Although I was able to get this to work (for some nominal definition of 'work'), I saw big problems looming on the horizon with this approach. Specifically, it's easy to migrate those changes once but what if it changes again before a save operation? Then I'd be stuck maintaining a long lived mapping of OIDs in the source context to OIDs in the destination context(s). This got ugly really fast. If anyone's interested, here's what I came up with:

@interface NSManagedObjectContext (MergeChangesFromObjectsDidChangeNotification)
- (void)mergeChangesFromObjectsDidChangeNotification: (NSNotification*)notification;
@end

@implementation NSManagedObjectContext (MergeChangesFromObjectsDidChangeNotification)

- (void)mergeChangesFromObjectsDidChangeNotification: (NSNotification*)notification
{
    if (![NSManagedObjectContextObjectsDidChangeNotification isEqual: notification.name])
        return;

    if (notification.object == self)
        return;

    NSManagedObjectContext* sourceContext = (NSManagedObjectContext*)notification.object;

    NSAssert(self.persistentStoreCoordinator == sourceContext.persistentStoreCoordinator, @"Can't merge changes between MOCs with different persistent store coordinators.");

    [sourceContext lock];

    // Create object in the local context to correspond to inserted objects...
    NSMutableDictionary* foreignOIDsToLocalOIDs = [NSMutableDictionary dictionary];
    for (NSManagedObject* foreignMO in [[notification userInfo] objectForKey: NSInsertedObjectsKey])
    {
        NSManagedObjectID* foreignOID = foreignMO.objectID;
        NSManagedObject* localMO = [[[NSManagedObject alloc] initWithEntity: foreignMO.entity insertIntoManagedObjectContext: self] autorelease];
        [foreignOIDsToLocalOIDs setObject: localMO.objectID forKey: foreignOID];
    }

    // Bring over all the attributes and relationships...
    NSMutableSet* insertedOrUpdated = [NSMutableSet set];
    [insertedOrUpdated unionSet: [[notification userInfo] objectForKey: NSInsertedObjectsKey]];
    [insertedOrUpdated unionSet: [[notification userInfo] objectForKey: NSUpdatedObjectsKey]];

    for (NSManagedObject* foreignMO in insertedOrUpdated)
    {
        NSManagedObjectID* foreignOID = foreignMO.objectID;
        NSManagedObjectID* localOID = [foreignOIDsToLocalOIDs objectForKey: foreignOID];
        localOID = localOID ? localOID : foreignOID;
        NSManagedObject* localMO = [self objectWithID: localOID];

        // Do the attributes.
        [localMO setValuesForKeysWithDictionary: [foreignMO dictionaryWithValuesForKeys: [[foreignMO.entity attributesByName] allKeys]]];

        // Do the relationships.
        NSDictionary* rByName = foreignMO.entity.relationshipsByName;
        for (NSString* key in [rByName allKeys])
        {
            NSRelationshipDescription* desc = [rByName objectForKey: key];
            if (!desc.isToMany)
            {
                NSManagedObject* relatedForeignMO = [foreignMO valueForKey: key];
                NSManagedObjectID* relatedForeignOID = relatedForeignMO.objectID;
                NSManagedObjectID* relatedLocalOID = [foreignOIDsToLocalOIDs objectForKey: relatedForeignOID];
                relatedLocalOID = relatedLocalOID ? relatedLocalOID : relatedForeignOID;
                NSManagedObject* localRelatedMO = [self objectWithID: relatedLocalOID];
                [localMO setValue: localRelatedMO forKey: key];
            }
            else
            {
                id collection = [foreignMO valueForKey: key];
                id newCollection = [NSMutableSet set];
                if ([collection isKindOfClass: [NSOrderedSet class]])
                {
                    newCollection = [NSOrderedSet orderedSet];
                }

                for (NSManagedObject* relatedForeignMO in collection)
                {
                    NSManagedObjectID* relatedForeignOID = relatedForeignMO.objectID;
                    NSManagedObjectID* relatedLocalOID = [foreignOIDsToLocalOIDs objectForKey: relatedForeignOID];
                    relatedLocalOID = relatedLocalOID ? relatedLocalOID : relatedForeignOID;
                    NSManagedObject* localRelatedMO = [self objectWithID: relatedLocalOID];
                    [newCollection addObject: localRelatedMO];
                }
                [localMO setValue: newCollection forKey: key];
            }
        }
    }

    // And delete any objects which pre-existed in my context.
    for (NSManagedObject* foreignMO in [[notification userInfo] objectForKey: NSDeletedObjectsKey])
    {
        NSManagedObjectID* foreignOID = foreignMO.objectID;
        NSManagedObject* localMO = [self existingObjectWithID: foreignOID error: NULL];
        if (localMO)
        {
            [self deleteObject: localMO];
        }
    }

    [sourceContext unlock];
}

@end

Conclusion

Between the improvements in concurrency management and this parent/child feature, I quickly lost interest pursuing a pre-Lion solution. I'm beginning to gather that the pre-Lion solution would effectively be "Don't use NSPersistentDocument." As best I can tell, all these pain points go away if I drop that requirement. Without that, you can save contexts and migrate stores whenever you want, but naturally you would have to do all that work yourself.

2
votes

The problem here is that the default NSManagedObjectContext that is created by the NSPersistentDocument is of concurrency type NSConfinementConcurrencyType. CoreData does not allow to create a child context of a context with that type.

As a workaround, this is working for me. The NSManagedObjectContext is created by your NSPersistentDocument, so you can override that method:

- (NSManagedObjectContext *)managedObjectContext {
    if (!_context) {
        NSManagedObjectContext *_default = [super managedObjectContext];
        _context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        _context.persistentStoreCoordinator = _default.persistentStoreCoordinator;
    }

    return _context;
}

I had no idea where to get the persistentStoreCoordinator from, so I called the super implementation and get it there from. With this context you should be able to create child context from that you can use in background operations.

1
votes

If you don't have a store then you can't save. It seems that you want to save the document in order to merge the changes made on a background thread; well, you can merge those changes manually. When the background thread completes, tell the main thread which objects have been updated / inserted and then make the same changes on the main thread.

If the changes are near arbitrary and thus tedious to duplicate, you can even construct your own NSManagedObjectContextDidSaveNotification on the background thread and then merge it using -[NSManagedObjectContext mergeChangesFromContextDidSaveNotification:] on the main thread.

0
votes

I found a very nicely written-up solution along the lines of ipmcc's solution (also running the NSPersistentDocument's MOC in a background thread, and making it the parent context of the main thread's MOC) here.

It includes the full BSD-licensed code for a document app based on that (basically a Mac OS version of iOS's UIManagedDocument).