Undoing Core Data insertions that are performed off the main thread
Asked Answered
D

5

17

I'm working on some code that uses an NSOperation to import data. I'd like for the user to be able to undo the NSManagedObject instances that are created during the import operation.

From what I can tell, it's impossible to use the NSManagedObjectContext -undoManager for any operations that are performed off of the main thread. From the Core Data Programming Guide section on Use Thread Confinement to Support Concurrency, we have these two conditions:

  1. Only objectID should be passed between managed object contexts (on separate threads)
  2. Managed objects must be saved in a context before the objectID can be used.

This makes sense since the managed objects need to be moved from private storage (NSManagedObjectContext) to public storage (NSPersistentStore) before they can be shared.

Unfortunately, the -save: message also causes any managed objects in the undo stack to be removed. From the Memory Management Using Core Data section of the same guide:

Managed objects that have pending changes (insertions, deletions, or updates) are retained by their context until their context is sent a save:, reset , rollback, or dealloc message, or the appropriate number of undos to undo the change.

I've tried several things to work around this limitation, and everything eventually leads back to bulk of the work happening on the main thread (and spinning beach balls.) Any clues to getting undo working with objects created off the main thread would be very much appreciated.

--

An enhancement Radar has been submitted: rdar://problem/8977725

Dragrope answered 9/2, 2011 at 2:23 Comment(2)
Why not just keep a log of all managed object ID's inserted by this operation? That way if the user wants to roll it back you can simply iterate over and delete the objects. If you must also roll back mutations, it's not too hard to keep a record of the key / value changes as well. Seems simpler than trying to shoe-horn this into the Undo Manager.Williemaewillies
In regards to the above comment, how would you suggest handing redo?Avaria
V
2

This answer will probably be a bit of a back and forth. If I understand the issue correctly, you are doing an import but when the import is done you want the user to be able to select what gets saved from the import?

If that is not correct, please fix my assumptions and I will update this answer.

If it is correct then what you can do is:

  1. Change your background object creation to

    NSEntityDescription *myEntity = ... //Entity from your context
    [[NSManagedObject alloc] initWithEntity:myEntity
             insertIntoManagedObjectContext:nil];
    
  2. Store these entities in an array.
  3. Pass the entities back to your main thread as needed.
  4. Release on any objects you don't want to keep
  5. Call [myMainContext insertObject:managedObject] on any you want to keep.
  6. Perform a save on the NSManagedObjectContext.

Since these entities are not part of a NSManagedObjectContext yet they only exist in memory and should be thread safe since they are not yet tied down to a NSManagedObjectContext.

This is of course theoretical and will require testing. However it should accomplish your goal.

Volkan answered 10/2, 2011 at 7:44 Comment(4)
Is [initWithEntity:entity insertIntoManagedObjectContext:nil] correct and working code? Aren't you supposed to create an in-memory MOC for those temporary objects?This
It is not necessary. You need a MOC to resolve the NSEntityDescription but the MO does not need to be associated with a MOC to exist. It only needs to be associated with a MOC to be saved.Volkan
Thanks for the response, Marcus! I hadn't thought about this approach, but I don't think it will work in this case: some of the entities created during the import are created conditionally (by a NSFetchRequest that queries managed objects in the context.) I had experimented with moving these queries off to the main thread, but getting the results across thread boundaries is tricky (-performSelectorOnMainThread returns a void.)Dragrope
One approach that I'm investigating now is to lock the NSManagedObjectContext in the main thread while the background import operation is working with the same instance. My goal here is to get the objects into the context without a -save: (so that undo will work) AND to not get any spinning beach balls (because there are a lot of files to import.) There's no work on the main thread that needs to happen during the import (other than updating progress indicators.)Dragrope
L
0

Not an expert, but I think what you're going to need to do is create a second context to perform the operations, then merge the two contexts together. You should be able to manage the merge as an undo step. Note, this only works if you're treating the entire set of operations as one undo step, as far as the user is concerned.

Lazare answered 9/2, 2011 at 3:20 Comment(1)
I'm already doing a -mergeChangesFromContextDidSaveNotification: to propagate the changes in the import context to the context on the main thread. The problem is that the merge adjusts the object graph with data that is already in the persistent store. The NSManagedObjectContext on the main thread doesn't have any -insertedObjects or -updatedObjects that need to be written to the persistent store.Dragrope
C
0

Suppose that you use a separate context for the background thread, and once it's done, push a [[backgroundContext undoManager] undo] onto the foreground thread's undo stack? I've never tried anything like that, but off the top of my head I can't think of a reason it shouldn't work.

Clippard answered 9/2, 2011 at 3:26 Comment(3)
What do you mean by "[[backgroundContext undoManager] undo] onto the foreground thread's undo stack"? From what I can tell the contents of the undo stack are private…Dragrope
[[[foregroundContext undoManager] prepareWithInvocationTarget:[backgroundContext undoManager]] undo]; (Actually, you'll need to save backgroundContext too...)Clippard
There are a couple of problems with this: the backgroundContext is a short-lived thread that the foregroundContext wouldn't be able to target in the long term. Also, as soon as the backgroundContext sends the -save: there's nothing on the undo stack for the foreground task to work with.Dragrope
R
0

One option may be to make your import thread persistent. Even when the thread is finished importing, it goes into an idle loop state. This way your threaded ManagedObjectContext is persisted in the proper thread. Then when the user wishes to undo a change, send a message to the thread to use the undomanager.

Roo answered 9/2, 2011 at 14:19 Comment(1)
The problem here is that the managed object contexts don't know about each other's inserted objects until the -save: happens. At which point the undo stack is torn down...Dragrope
A
0

It's incredibly likely you've considered this and you're likely only looking for a solution using the existing undoManager, but just in case:

Since you're inserting objects and not updating existing ones, you do have the power to tag them with a transaction id as each batch is imported, deleting them in a background thread in the case of an undo. A simple incremented NSNumber is sufficient for the tag.

Inelegant, but workable.

Aeneas answered 10/2, 2011 at 7:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.