How do I copy or move an NSManagedObject from one context to another?
Asked Answered
W

5

57

I have what I assume is a fairly standard setup, with one scratchpad MOC which is never saved (containing a bunch of objects downloaded from the web) and another permanent MOC which persists objects. When the user selects an object from scratchMOC to add to her library, I want to either 1) remove the object from scratchMOC and insert into permanentMOC, or 2) copy the object into permanentMOC. The Core Data FAQ says I can copy an object like this:

NSManagedObjectID *objectID = [managedObject objectID];
NSManagedObject *copy = [context2 objectWithID:objectID];

(In this case, context2 would be permanentMOC.) However, when I do this, the copied object is faulted; the data is initially unresolved. When it does get resolved, later, all of the values are nil; none of the data (attributes or relationships) from the original managedObject are actually copied or referenced. Therefore I can't see any difference between using this objectWithID: method and just inserting an entirely new object into permanentMOC using insertNewObjectForEntityForName:.

I realize I can create a new object in permanentMOC and manually copy each key-value pair from the old object, but I'm not very happy with that solution. (I have a number of different managed objects for which I have this problem, so I don't want to have to write and update copy: methods for all of them as I continue developing.) Is there a better way?

Wailoo answered 8/6, 2010 at 15:10 Comment(0)
A
57

First, having more than one NSManagedObjectContext on a single thread is not a standard configuration. 99% of the time you only need one context and that will solve this situation for you.

Why do you feel you need more than one NSManagedObjectContext?

Update

That is actually one of the few use cases that I have seen where that makes sense. To do this, you need to do a recursive copy of the object from one context to the other. The workflow would be as follows:

  1. Create new object in persistent context
  2. get a dictionary of the attributes from the source object (use -dictionaryWithValuesForKeys and -[NSEntityDescription attributesByName] to do this.
  3. set the dictionary of values onto the target object (using -setValuesForKeysWithDictionary)
  4. If you have relationships, you will need to do this copy recursively and walk the relationships either hard coded (to avoid some circular logic) or by using the -[NSEntityDescription relationshipsByName]

As mentioned by another, you can download the sample code from my book from The Pragmatic Programmers Core Data Book and see one solution to this problem. Of course in the book I discuss it more in depth :)

Avulsion answered 9/6, 2010 at 17:54 Comment(11)
My app has a mapView which downloads hundreds or thousands of objects to use as annotations whenever the app starts (or the user changes location). My idea was that it would be more elegant and more efficient to keep the map objects in a second MOC (scratchMOC), which would never be saved; by design, it's thrown out and reloaded whenever the app starts up. The user can then choose one or more of the map objects to add to his library, and these would then get copied/moved to the persisted MOC. I figured this would avoid iterating through a MOC to delete the thousands of map objects at app exit.Wailoo
For some reason I had trouble getting the right code to execute step 2, so here it is: NSDictionary *newValues = [oldObject dictionaryWithValuesForKeys:[[objectEntity attributesByName] allKeys]]Grazing
@Marcus - Could you please elaborate why should not one prefer using more than one NSManagedObjectContext in a single thread? Are there any implications of it if we do so? Thanks!Figure
There are no implications, it is just wasteful. Unless you are running into a few very specific situations there is rarely any need to introduce the additional complexity that comes with having multiple MOCs on the same thread. Since all MOs are tied to their own MOC you can run into issues with relationships, look ups, etc. being inconsistent when you start adding more MOCs to your app. It is rarely worth the headache.Avulsion
@Marcus is that opinion still valid with the CoreData improvements like parent-child MOCs now? Because it seems to me now that using multiple MOCs is not just okay, but recommended, even for simple cases like creating a new object then choosing to cancel/save. The Apple sample CoreDataBooks now does this with multiple MOCs.Lobotomy
@Lobotomy With the new parent/child MOCs it is starting to make sense to use them in a single thread situation, especially when you are creating a new object and want the ability to just cancel out of it.Avulsion
And here's a category I wrote that seems to work. gist.github.com/steakknife/3bae589c4da8eba9b361Coarsegrained
@MarcusS.Zarra having a child MOC as scratchMOC and only some MOs from this scratchMOC needs to be persist. For this still this work flow valid? Or you find better options in new SDKs to move selected MOs between MOCs on the same thread.Transmissible
Saving is at the MOC level so no that will not work very well @MadNik. It is an all or nothing affair so you would need to delete the objects that do not need to be persisted or do not create them as MOs in the first place.Avulsion
"NSManagedObjectContext on a single thread is not a standard " -- totally not trueGenarogendarme
You should show the whole quote: "having more than one NSManagedObjectContext on a single thread is not a standard configuration". That is a true statement. There is ZERO reason to have more than one context associated with the same thread or queue.Avulsion
T
10

The documentation is misleading and incomplete. The objectID methods do not themselves copy objects they simply guarantee that you've gotten the specific object you wanted.

The context2 in the example is in fact the source context and not the destination. You're getting a nil because the destination context has no object with that ID.

Copying managed objects is fairly involved owing to the complexity of the object graph and the way that the context manage the graph. You do have to recreate the copied object in detail in the new context.

Here is some example code that I snipped from the example code for The Pragmatic Programmer's Core Data: Apple's API for Persisting Data on Mac OS X. (You might be able to download the entire project code without buying the book at the Pragmatic site.) It should provide you with a rough idea of how to go about copying an object between context.

You can create some base code that copies objects but the particulars of each object graph's relationships usually mean that you have to customize for each data model.

Tracheitis answered 8/6, 2010 at 16:41 Comment(3)
I like your example code, but I can't figure out what "self" is. copyObject is object to be copied. toContext target context. parentEntity superclass. But what is [self lookup] what class is self? Where does this code live?Sparklesparkler
Ok, think I got it. The lookup is just a dictionary to avoid duplicating the same object twice when traversing the object graph.Sparklesparkler
Hai, first of all, thanks for the snippet, it's awesome! When I use this code on an example (Event and Device models, Event has creatingDevice, Device has events) I fetch all the events, then call your function, then call save() on the new context and it throws an exception saying creatingDevice is nil... What do I do wrong? Thanks!Mandola
C
7

Had the same problem myself and found this article on created disconnected entities that could later be added to the context: http://locassa.com/temporary-storage-in-apples-coredata/

The idea is that you have an NSManagedObject because you're going to be storing objects in the database. My hurdle was that many of these objects are downloaded via HTTP API and I want to throw out most of them at the end of the session. Think of a stream of user posts, and I only want to save the ones that were favorited or saved as a draft.

I create all of my posts using

+ (id)newPost {
    NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"Post" inManagedObjectContext:self.managedObjectContext];
    Post *post = [[Post alloc] initWithEntity:entityDescription insertIntoManagedObjectContext:nil];
    return post;
}

and then the posts are inserted to the local managed object context when they are favorited

+ (BOOL)favoritePost:(Post *)post isFavorite:(BOOL)isFavorite
{
    // Set the post's isFavorite flag
    post.isFavorite = [NSNumber numberWithBool:isFavorite];

    // If the post is being favorited and not yet in the local database, add it
    NSError *error;
    if (isFavorite && [self.managedObjectContext existingObjectWithID:post.objectID error:&error] == nil) {
        [self.managedObjectContext insertObject:post];
    }
    // Else if the post is being un-favorited and is in the local database, delete it
    else if (!isFavorite && [self.managedObjectContext existingObjectWithID:post.objectID error:&error] != nil) {
        [self.managedObjectContext deleteObject:post];
    }

    // If there was an error, output and return NO to indicate a failure
    if (error) {
        NSLog(@"error: %@", error);
        return NO;
    }

    return YES;
}

Hope that helps.

Cumulostratus answered 12/7, 2012 at 13:55 Comment(1)
Note on 'insertIntoManagedObjectContext:nil': This seems to have been deprecated in iOS8.Frog
L
1

You need to make sure that you're saving the context that managedObject lives in. In order to fetch the same object in a different context, it needs to be present in the persistent store.

According to the documentation, objectWithID: always returns an object. So the fact that the fault resolves to an object will all nil values implies that it's not finding your object in the persistent store.

Leena answered 8/6, 2010 at 15:27 Comment(3)
Just confirmed that this works as advertised--thanks! However, the reason I wanted a second context (scratchMOC) in the first place is actually to avoid saving any of the objects; I want everything to be thrown out when the user exits and reloaded again when the app starts. Guess that may not be possible.Wailoo
Take a look at the in-memory store type. It's a store that only lives in memory and goes away when the program exits. Just be careful that you can't have relationships across stores.Leena
Alex, that looks very tempting, but I'm having a little trouble figuring out how it works. Are the two stores (persistent and in-memory) in two separate contexts or in the same one?Wailoo
T
1

Swift 5

If you're wondering how to copy NSManagedObjects in 2020, the code below works for me:

// `Restaurant` is the name of my managed object subclass.

// I like to have Xcode auto generate my subclasses (Codegen 
//     set to "Class Definition") & then just extend them with 
//     whatever functionality I need.

extension Restaurant {
    public func copy() -> Restaurant? {
        let attributes = entity.attributesByName.map { $0.key }
        let dictionary = dictionaryWithValues(forKeys: attributes)
        guard let context = AppDelegate.shared?.persistentContainer.viewContext,
            let restaurantCopy = NSEntityDescription.insertNewObject(forEntityName: Restaurant.entityName, into: context) as? Restaurant
            else
        {
            return nil
        }
        restaurantCopy.setValuesForKeys(dictionary)

        return restaurantCopy
    }
}
Toweling answered 30/1, 2020 at 23:39 Comment(1)
where is relationships?Leena

© 2022 - 2024 — McMap. All rights reserved.