How can I duplicate, or copy a Core Data Managed Object?
Asked Answered
I

17

68

I have a managed object ("A") that contains various attributes and types of relationships, and its relationships also have their own attributes & relationships. What I would like to do is to "copy" or "duplicate" the entire object graph rooted at object "A", and thus creating a new object "B" that is very similar to "A".

To be more specific, none of the relationships contained by "B" (or its children) should point to objects related to "A". There should be an entirely new object graph with similar relationships intact, and all objects having the same attributes, but of course different id's.

There is the obvious manual way to do this, but I was hoping to learn of a simpler means of doing so which was not totally apparent from the Core Data documentation.

TIA!

Ism answered 28/4, 2010 at 15:22 Comment(2)
This is awesome, and everything, but now there are now 13 different forks of the same algorithm here, each with its own interesting features and fixes! … S.O. should perhaps provide a git repo for each question to address this :-)Nalepka
I think there is no "obvious manual" way to do this, since an NSManagedObject is part of an arbitrarily complicated graph of relations. You will need to address circular relations, back-relation, and "meeting" the same entities again and again. This question maps to the question of "copying sub-graphs". My experience with many CoreData models of all complexities is that you usually want a "Shallow" cloner, that will clone the entity and its attributes - and also clone its relations. not the related entities. Meaning - the original will relate to the same entities as the clone.Keratitis
S
61

Here's a class I created to perform a "deep copy" of managed objects: attributes and relationships. Note that this does not check against loops in the object graph. (Thanks Jaanus for the starting off point...)

@interface ManagedObjectCloner : NSObject {
}
+(NSManagedObject *)clone:(NSManagedObject *)source inContext:(NSManagedObjectContext *)context;
@end

@implementation ManagedObjectCloner

+(NSManagedObject *) clone:(NSManagedObject *)source inContext:(NSManagedObjectContext *)context{
    NSString *entityName = [[source entity] name];

    //create new object in data store
    NSManagedObject *cloned = [NSEntityDescription
                               insertNewObjectForEntityForName:entityName
                               inManagedObjectContext:context];

    //loop through all attributes and assign then to the clone
    NSDictionary *attributes = [[NSEntityDescription
                                 entityForName:entityName
                                 inManagedObjectContext:context] attributesByName];

    for (NSString *attr in attributes) {
        [cloned setValue:[source valueForKey:attr] forKey:attr];
    }

    //Loop through all relationships, and clone them.
    NSDictionary *relationships = [[NSEntityDescription
                                   entityForName:entityName
                                   inManagedObjectContext:context] relationshipsByName];
    for (NSRelationshipDescription *rel in relationships){
        NSString *keyName = [NSString stringWithFormat:@"%@",rel];
        //get a set of all objects in the relationship
        NSMutableSet *sourceSet = [source mutableSetValueForKey:keyName];
        NSMutableSet *clonedSet = [cloned mutableSetValueForKey:keyName];
        NSEnumerator *e = [sourceSet objectEnumerator];
        NSManagedObject *relatedObject;
        while ( relatedObject = [e nextObject]){
            //Clone it, and add clone to set
            NSManagedObject *clonedRelatedObject = [ManagedObjectCloner clone:relatedObject 
                                                          inContext:context];
            [clonedSet addObject:clonedRelatedObject];
        }

    }

    return cloned;
}


@end
Showroom answered 29/5, 2010 at 21:30 Comment(8)
This is terrific! My only snag: UITableView does not always animate in the (effectively new) cell properly. I wonder if it concerns insertNewObjectForEntityForName:inManagedObjectContext:, THEN performing a deep copy, which may instigate more NSFetchedResultsControllerDelegate messages (?). I don't have any loops, and the data I'm copying isn't very deep per se, so hopefully that's good. Perhaps there's some way for me to somehow "build up" the entire cloned object, and THEN insert it in one fell swoop, or at least defer notification that an add took place. Looking to see if that's doable.Gaiety
this is fantastic! One note, however. The line 'for (NSRelationshipDescription *rel in relationships)' is not correct. relationships is an NSDictionary. I've modified to grab the NSRelationshipDescription 'for (NSString *relName in [relationships allKeys]){ NSRelationshipDescription *rel = [relationships objectForKey:relName]; ' and to check for "isToMany"Gaffe
I'm getting this error. Any pointers? *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSCFSet: 0x7003f80> was mutated while being enumerated.<CFBasicHash 0x7003f80 [0x1314400]>Underpainting
Note: I have a multi-level complex graph with one-to-one and one-to-many relationships. I've applied the fix suggested by levous for one-to-one relationships, and it seems to be working, but overall the routine is failing.Underpainting
@Underpainting I know it was months ago now, but I was having the same problem with the mutated set. Check my answer for a possible solution.Bellanca
NSString *keyName = [NSString stringWithFormat:@"%@",rel] does not contain the relationships name in iOS6, use [NSString stringWithFormat:@"%@",rel.name]. Otherwise keyName contains the whole description string for the relationHoelscher
it worked! but when I change the source to nil, the clone is also changing! how is that possible?!?!Stakeout
excuse me, what about thread safety? I have a pretty large database and I don't want to block the main thread, but contexts are tied to their own queue nowadaysTicktack
A
43

These answers got me really close, though they did seem to have some shortcomings:

1st, I took the advice of Z S and made it a category on NSManagedObject, this seemed a little cleaner to me.

2nd, My object graph contains to-one relationships, so I started from levous's example, but note that levous's example isn't cloning the object in the case of the to-one relationship. This will cause a crash (attempting to save a NSMO from one context in a different context). I have addressed that in the example below.

3rd, I provided a cache of already-cloned objects, this prevents objects from being cloned twice and therefore duplicated in the new object graph, and also prevents cycles.

4th, I've added a black-list (list of Entity-types not to clone). I did this in part to solve one shortcoming of my final solution, which I will describe below.

NOTE: if you use what I understand to a be CoreData best-practice, always providing inverse relationships, then this will likely clone all objects that have a relationship to the object you want to clone. If you are using inverses and you have a single root object that knows about all other objects, then you will likely clone the whole thing. My solution to this was to add the blacklist and pass in the Entity type that I knew was a parent of one of the objects I wanted cloned. This appears to work for me. :)

Happy cloning!

// NSManagedObject+Clone.h
#import <CoreData/CoreData.h>

@interface NSManagedObject (Clone)
- (NSManagedObject *)cloneInContext:(NSManagedObjectContext *)context exludeEntities:(NSArray *)namesOfEntitiesToExclude;
@end


// NSManagedObject+Clone.m
#import "NSManagedObject+Clone.h"

@implementation NSManagedObject (Clone)

- (NSManagedObject *)cloneInContext:(NSManagedObjectContext *)context withCopiedCache:(NSMutableDictionary *)alreadyCopied exludeEntities:(NSArray *)namesOfEntitiesToExclude {
  NSString *entityName = [[self entity] name];

  if ([namesOfEntitiesToExclude containsObject:entityName]) {
    return nil;
  }

  NSManagedObject *cloned = [alreadyCopied objectForKey:[self objectID]];
  if (cloned != nil) {
    return cloned;
  }

  //create new object in data store
  cloned = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:context];
  [alreadyCopied setObject:cloned forKey:[self objectID]];

  //loop through all attributes and assign then to the clone
  NSDictionary *attributes = [[NSEntityDescription entityForName:entityName inManagedObjectContext:context] attributesByName];

  for (NSString *attr in attributes) {
    [cloned setValue:[self valueForKey:attr] forKey:attr];
  }

  //Loop through all relationships, and clone them.
  NSDictionary *relationships = [[NSEntityDescription entityForName:entityName inManagedObjectContext:context] relationshipsByName];
  for (NSString *relName in [relationships allKeys]){
    NSRelationshipDescription *rel = [relationships objectForKey:relName];

    NSString *keyName = rel.name;
    if ([rel isToMany]) {
      //get a set of all objects in the relationship
      NSMutableSet *sourceSet = [self mutableSetValueForKey:keyName];
      NSMutableSet *clonedSet = [cloned mutableSetValueForKey:keyName];
      NSEnumerator *e = [sourceSet objectEnumerator];
      NSManagedObject *relatedObject;
      while ( relatedObject = [e nextObject]){
        //Clone it, and add clone to set
        NSManagedObject *clonedRelatedObject = [relatedObject cloneInContext:context withCopiedCache:alreadyCopied exludeEntities:namesOfEntitiesToExclude];
        [clonedSet addObject:clonedRelatedObject];
      }
    }else {
      NSManagedObject *relatedObject = [self valueForKey:keyName];
      if (relatedObject != nil) {
        NSManagedObject *clonedRelatedObject = [relatedObject cloneInContext:context withCopiedCache:alreadyCopied exludeEntities:namesOfEntitiesToExclude];
        [cloned setValue:clonedRelatedObject forKey:keyName];
      }
    }
  }

  return cloned;
}

- (NSManagedObject *)cloneInContext:(NSManagedObjectContext *)context exludeEntities:(NSArray *)namesOfEntitiesToExclude {
  return [self cloneInContext:context withCopiedCache:[NSMutableDictionary dictionary] exludeEntities:namesOfEntitiesToExclude];
}

@end
Airtoair answered 30/9, 2011 at 16:35 Comment(7)
This worked like a charm for me. I had both to-many and to-one relationships. I had objects in the hierarchy I did not want cloned. My tests indicate it worked fine. I owe you a beer. FYI, I did convert back to class method from category for app architecture reasons. I also swapped copiedCache argument with excludeEntities to follow Apple naming guidelines of ever lengthening method names. And fixed your "exlude" misspelling.Pantheon
This code works in iOS6 with ARC for nested relationship and using RestKit! ThanksPenitential
This is the one that was closest to working. However, some of my relationships are defined as "ordered", something new to iOS 6. So I am posting my modification that checks that.Hillery
This is the most complete implementation here.Kaleykaleyard
This is the best solution but there is one bug. You need to check if clonedRelatedObject is nil before adding it to clonedSet. If the related object was part of namesOfEntitiesToExclude it will be nil and throw an error when trying to add it to clonedSet.Distal
I've tried this but get this error this line. NSMutableSet *clonedSet = [cloned mutableSetValueForKey:keyName]; *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'NSManagedObjects of entity 'Form' do not support -mutableSetValueForKey: for the property 'contains''Bipetalous
@MarkBridges I know its late but anyways, I got this error because I had an ordered relation so I used solution by Dmitry Makarenko where ordered relation is also handled and it worked.Adkisson
G
24

I've updated user353759's answer to support toOne relationships.

@interface ManagedObjectCloner : NSObject {
}
+(NSManagedObject *)clone:(NSManagedObject *)source inContext:(NSManagedObjectContext *)context;
@end

@implementation ManagedObjectCloner

+(NSManagedObject *) clone:(NSManagedObject *)source inContext:(NSManagedObjectContext *)context{
    NSString *entityName = [[source entity] name];

    //create new object in data store
    NSManagedObject *cloned = [NSEntityDescription
                               insertNewObjectForEntityForName:entityName
                               inManagedObjectContext:context];

    //loop through all attributes and assign then to the clone
    NSDictionary *attributes = [[NSEntityDescription
                                 entityForName:entityName
                                 inManagedObjectContext:context] attributesByName];

    for (NSString *attr in attributes) {
        [cloned setValue:[source valueForKey:attr] forKey:attr];
    }

    //Loop through all relationships, and clone them.
    NSDictionary *relationships = [[NSEntityDescription
                                    entityForName:entityName
                                    inManagedObjectContext:context] relationshipsByName];
    for (NSString *relName in [relationships allKeys]){
        NSRelationshipDescription *rel = [relationships objectForKey:relName];

        NSString *keyName = [NSString stringWithFormat:@"%@",rel];
        if ([rel isToMany]) {
            //get a set of all objects in the relationship
            NSMutableSet *sourceSet = [source mutableSetValueForKey:keyName];
            NSMutableSet *clonedSet = [cloned mutableSetValueForKey:keyName];
            NSEnumerator *e = [sourceSet objectEnumerator];
            NSManagedObject *relatedObject;
            while ( relatedObject = [e nextObject]){
                //Clone it, and add clone to set
                NSManagedObject *clonedRelatedObject = [ManagedObjectCloner clone:relatedObject 
                                                                        inContext:context];
                [clonedSet addObject:clonedRelatedObject];
            }
        }else {
            [cloned setValue:[source valueForKey:keyName] forKey:keyName];
        }

    }

    return cloned;
}
Gaffe answered 15/3, 2011 at 17:0 Comment(3)
One small improvement might be to make it a category on NSManagedObject, so you don't have to pass in the context and the source object.Hepzi
So if I have ObjectA and ObjectB, I can copy all the entries in ObjectA and paste them into ObjectB? Then delete ObjectA and have just ObjectB with all my entries? Is that the purpose of a "deep copy"? Because that's what I'm looking for.Perse
Does not work for my RestKit project in iOS6 with ARC. I get EXC_BAD_ACCESS at NSEnumerator *e = [sourceSet objectEnumerator];Penitential
D
18

Swift 5

This builds upon @Derrick & @Dmitry Makarenko & @masonk's contributions, bringing everything together, improving on it and turning it into a solution fit for 2020.

  • Handles both 1-to-1 & 1-to-Many realtionships
  • Handles ordered relationships
  • Copies the entire NSManagedObject graph (using an alreadyCopied cache)
  • Implemented as an extension to NSManagedObject

.

extension NSManagedObject {

    func copyEntireObjectGraph(context: NSManagedObjectContext) -> NSManagedObject {
    
        var cache = Dictionary<NSManagedObjectID, NSManagedObject>()
        return cloneObject(context: context, cache: &cache)
    
    }

    func cloneObject(context: NSManagedObjectContext, cache alreadyCopied: inout Dictionary<NSManagedObjectID, NSManagedObject>) -> NSManagedObject {
    
        guard let entityName = self.entity.name else {
            fatalError("source.entity.name == nil")
        }
    
        if let storedCopy = alreadyCopied[self.objectID] {
            return storedCopy
        }
    
        let cloned = NSEntityDescription.insertNewObject(forEntityName: entityName, into: context)
    alreadyCopied[self.objectID] = cloned

        if let attributes = NSEntityDescription.entity(forEntityName: entityName, in: context)?.attributesByName {

            for key in attributes.keys {
                cloned.setValue(self.value(forKey: key), forKey: key)
            }
        
        }

        if let relationships = NSEntityDescription.entity(forEntityName: entityName, in: context)?.relationshipsByName {
    
            for (key, value) in relationships {
            
                if value.isToMany {

                    if let sourceSet = self.value(forKey: key) as? NSMutableOrderedSet {
                
                        guard let clonedSet = cloned.value(forKey: key) as? NSMutableOrderedSet else {
                            fatalError("Could not cast relationship \(key) to an NSMutableOrderedSet")
                        }
                    
                        let enumerator = sourceSet.objectEnumerator()

                        var nextObject = enumerator.nextObject() as? NSManagedObject

                        while let relatedObject = nextObject {
                        
                            let clonedRelatedObject = relatedObject.cloneObject(context: context, cache: &alreadyCopied)
                            clonedSet.add(clonedRelatedObject)
                            nextObject = enumerator.nextObject() as? NSManagedObject
                        
                        }
                    
                    } else if let sourceSet = self.value(forKey: key) as? NSMutableSet {
                
                        guard let clonedSet = cloned.value(forKey: key) as? NSMutableSet else {
                            fatalError("Could not cast relationship \(key) to an NSMutableSet")
                        }
                    
                        let enumerator = sourceSet.objectEnumerator()

                        var nextObject = enumerator.nextObject() as? NSManagedObject

                        while let relatedObject = nextObject {
                        
                            let clonedRelatedObject = relatedObject.cloneObject(context: context, cache: &alreadyCopied)
                            clonedSet.add(clonedRelatedObject)
                            nextObject = enumerator.nextObject() as? NSManagedObject
                        
                        }
                    
                    }
            
                } else {
                
                    if let relatedObject = self.value(forKey: key) as? NSManagedObject {
                
                        let clonedRelatedObject = relatedObject.cloneObject(context: context, cache: &alreadyCopied)
                        cloned.setValue(clonedRelatedObject, forKey: key)
                    
                    }
                
                }
            
            }
        
        }

        return cloned
    
    }

}

Usage:

let myManagedObjectCopy = myManagedObject.copyEntireObjectGraph(context: myContext)
Dorettadorette answered 23/12, 2019 at 5:56 Comment(3)
Could you clarify something - does this "clone" automatically get added to the database, or is it in "limbo" until is the context is saved?Mosa
there is no managedObject.save() in the code, so u have to save it yourself. how he's done it is the right way to do it.Preinstruct
It's a bit old but you saved me a lot of work. I confirm this works perfectly. Thanks!Dicker
H
16

This is @Derricks answer, modified to support the new-as-of iOS 6.0 ordered to-many relationships by interrogating the relationship to see if it is ordered. While I was there, I added a simpler -clone method for the common case of cloning within the same NSManagedObjectContext.

//
//  NSManagedObject+Clone.h
//  Tone Poet
//
//  Created by Mason Kramer on 5/31/13.
//  Copyright (c) 2013 Mason Kramer. The contents of this file are available for use by anyone, for any purpose whatsoever.
//

#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>

@interface NSManagedObject (Clone) {
}

-(NSManagedObject *)cloneInContext:(NSManagedObjectContext *)context withCopiedCache:(NSMutableDictionary *)alreadyCopied exludeEntities:(NSArray *)namesOfEntitiesToExclude;
-(NSManagedObject *)cloneInContext:(NSManagedObjectContext *)context exludeEntities:(NSArray *)namesOfEntitiesToExclude;
-(NSManagedObject *) clone;

@end

//
//  NSManagedObject+Clone.m
//  Tone Poet
//
//  Created by Mason Kramer on 5/31/13.
//  Copyright (c) 2013 Mason Kramer. The contents of this file are available for use by anyone, for any purpose whatsoever.
//


#import "NSManagedObject+Clone.h"

@implementation NSManagedObject (Clone)

-(NSManagedObject *) clone {
    return [self cloneInContext:[self managedObjectContext] exludeEntities:@[]];
}

- (NSManagedObject *)cloneInContext:(NSManagedObjectContext *)context withCopiedCache:(NSMutableDictionary *)alreadyCopied exludeEntities:(NSArray *)namesOfEntitiesToExclude {
    NSString *entityName = [[self entity] name];

    if ([namesOfEntitiesToExclude containsObject:entityName]) {
        return nil;
    }

    NSManagedObject *cloned = [alreadyCopied objectForKey:[self objectID]];
    if (cloned != nil) {
        return cloned;
    }

    //create new object in data store
    cloned = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:context];
    [alreadyCopied setObject:cloned forKey:[self objectID]];

    //loop through all attributes and assign then to the clone
    NSDictionary *attributes = [[NSEntityDescription entityForName:entityName inManagedObjectContext:context] attributesByName];

    for (NSString *attr in attributes) {
        [cloned setValue:[self valueForKey:attr] forKey:attr];
    }

    //Loop through all relationships, and clone them.
    NSDictionary *relationships = [[NSEntityDescription entityForName:entityName inManagedObjectContext:context] relationshipsByName];
    for (NSString *relName in [relationships allKeys]){
        NSRelationshipDescription *rel = [relationships objectForKey:relName];

        NSString *keyName = rel.name;
        if ([rel isToMany]) {
            if ([rel isOrdered]) {
                NSMutableOrderedSet *sourceSet = [self mutableOrderedSetValueForKey:keyName];
                NSMutableOrderedSet *clonedSet = [cloned mutableOrderedSetValueForKey:keyName];

                NSEnumerator *e = [sourceSet objectEnumerator];

                NSManagedObject *relatedObject;
                while ( relatedObject = [e nextObject]){
                    //Clone it, and add clone to set
                    NSManagedObject *clonedRelatedObject = [relatedObject cloneInContext:context withCopiedCache:alreadyCopied exludeEntities:namesOfEntitiesToExclude];


                    [clonedSet addObject:clonedRelatedObject];
                    [clonedSet addObject:clonedRelatedObject];
                }
            }
            else {
                NSMutableSet *sourceSet = [self mutableSetValueForKey:keyName];
                NSMutableSet *clonedSet = [cloned mutableSetValueForKey:keyName];
                NSEnumerator *e = [sourceSet objectEnumerator];
                NSManagedObject *relatedObject;
                while ( relatedObject = [e nextObject]){
                    //Clone it, and add clone to set
                    NSManagedObject *clonedRelatedObject = [relatedObject cloneInContext:context withCopiedCache:alreadyCopied exludeEntities:namesOfEntitiesToExclude];

                    [clonedSet addObject:clonedRelatedObject];
                }
            }
        }
        else {
            NSManagedObject *relatedObject = [self valueForKey:keyName];
            if (relatedObject != nil) {
                NSManagedObject *clonedRelatedObject = [relatedObject cloneInContext:context withCopiedCache:alreadyCopied exludeEntities:namesOfEntitiesToExclude];
                [cloned setValue:clonedRelatedObject forKey:keyName];
            }
        }

    }

    return cloned;
}

-(NSManagedObject *)cloneInContext:(NSManagedObjectContext *)context exludeEntities:(NSArray *)namesOfEntitiesToExclude {
    return [self cloneInContext:context withCopiedCache:[NSMutableDictionary dictionary] exludeEntities:namesOfEntitiesToExclude];
}
@end
Hillery answered 31/5, 2013 at 20:38 Comment(6)
Had a crash trying to use Derrick's answer but this one works for me.Bipetalous
This is great answer, must be promoted!Parliamentarian
This generally works, except in my case, the ordering of ordered relationships is reversed every time I copy! Any ideas? Also, is there a reason you do [clonedSet addObject:clonedRelatedObject]; twice for ordered relationships?Lotty
I think that the ordering here doesn't work well. It seems to have a bug that I fixed in my own variant. For ordered relationships that have an inverse, depth first cloning (as used in all these implementations) can recursively cause the other end of the relationship to be set, which updates the ordered set as you build it. The work around for this is to build the complete ordered set and then assign using the primitive KVO variant.Nalepka
@elsurudo: I think that is a typo.Gonzales
Bad typo then ;)Bondstone
B
8

I've noticed a couple of bugs with the current answers. Firstly, something seems to be mutating the set of to-Many related objects as its being iterated over. Secondly, I'm not sure if something has changed within the API, but using the NSRelationshipDescription's String representation as a key was throwing exceptions when grabbing those related objects.

I've made a few tweaks, have done some basic testing and it seems to work. If anyone wants to investigate further that would be great!

@implementation NSManagedObjectContext (DeepCopy)

-(NSManagedObject *) clone:(NSManagedObject *)source{
    NSString *entityName = [[source entity] name];

    //create new object in data store
    NSManagedObject *cloned = [NSEntityDescription
                               insertNewObjectForEntityForName:entityName
                               inManagedObjectContext:self];

    //loop through all attributes and assign then to the clone
    NSDictionary *attributes = [[NSEntityDescription
                                 entityForName:entityName
                                 inManagedObjectContext:self] attributesByName];

    for (NSString *attr in attributes) {
        [cloned setValue:[source valueForKey:attr] forKey:attr];
    }

    //Loop through all relationships, and clone them.
    NSDictionary *relationships = [[NSEntityDescription
                                    entityForName:entityName
                                    inManagedObjectContext:self] relationshipsByName];

    for (NSString *relName in [relationships allKeys]){

        NSRelationshipDescription *rel = [relationships objectForKey:relName];
        if ([rel isToMany]) {
            //get a set of all objects in the relationship
            NSArray *sourceArray = [[source mutableSetValueForKey:relName] allObjects];
            NSMutableSet *clonedSet = [cloned mutableSetValueForKey:relName];
            for(NSManagedObject *relatedObject in sourceArray) {
                NSManagedObject *clonedRelatedObject = [self clone:relatedObject];
                [clonedSet addObject:clonedRelatedObject];
            }
        } else {
            [cloned setValue:[source valueForKey:relName] forKey:relName];
        }

    }

    return cloned;
}

@end
Bellanca answered 10/11, 2011 at 11:34 Comment(2)
[cloned setValue:[source valueForKey:relName] forKey:relName]; does not clone the target object in the to-one-relation and leads to an exception, because you might try to link objects from different contexts. It is also inconsistent with the toMany branch. It is probably a typo/minor oversight.Purveyor
There are lots of different answers here, but this one worked with objects that had attributes that were both toone and tomany.Titter
I
6

Something like this? (untested) This would be the “manual way” you mention, but it would automatically be in syncwith model changes and such so you wouldn't have to manually enter all the attribute names.

Swift 3:

extension NSManagedObject {
    func shallowCopy() -> NSManagedObject? {
        guard let context = managedObjectContext, let entityName = entity.name else { return nil }
        let copy = NSEntityDescription.insertNewObject(forEntityName: entityName, into: context)
        let attributes = entity.attributesByName
        for (attrKey, _) in attributes {
            copy.setValue(value(forKey: attrKey), forKey: attrKey)
        }
        return copy
    }
}

Objective-C:

@interface MyObject (Clone)
- (MyObject *)clone;
@end

@implementation MyObject (Clone)

- (MyObject *)clone{

    MyObject *cloned = [NSEntityDescription
    insertNewObjectForEntityForName:@"MyObject"
    inManagedObjectContext:moc];

    NSDictionary *attributes = [[NSEntityDescription
    entityForName:@"MyObject"
    inManagedObjectContext:moc] attributesByName];

    for (NSString *attr in attributes) {
        [cloned setValue:[self valueForKey:attr] forKey:attr];
    }

    return cloned;
}

@end

This will return you a clone with all attributes and no relations copied over.

Ioved answered 28/4, 2010 at 15:50 Comment(4)
This doesn't do a "deep copy" of relationships that the OP wants.Ectoderm
Yep, this only copies attributes. Could also do propertiesByName and/or relationshipsByName, but I won't add them to my example because (unlike the above) I have not done this kind of relation copying myself and can't vouch for it.Ioved
This also no copies but references exist values to the attributes. Simply, ObjA's value being pointed to ObjB, meaning if value being modified affect ObjA.Heliolatry
who asked for shallow copy?Tindall
N
6

I have modified Derrick's answer, which worked perfectly for me, to support ordered relationships available in iOS 5.0 and Mac OS X 10.7:

//
//  NSManagedObject+Clone.h
//

#import <CoreData/CoreData.h>

#ifndef CD_CUSTOM_DEBUG_LOG
#define CD_CUSTOM_DEBUG_LOG NSLog
#endif

@interface NSManagedObject (Clone)

- (NSManagedObject *)cloneInContext:(NSManagedObjectContext *)context
                    excludeEntities:(NSArray *)namesOfEntitiesToExclude;

@end

//
//  NSManagedObject+Clone.m
//

#import "NSManagedObject+Clone.h"

@interface NSManagedObject (ClonePrivate)
- (NSManagedObject *)cloneInContext:(NSManagedObjectContext *)context
                    withCopiedCache:(NSMutableDictionary **)alreadyCopied
                    excludeEntities:(NSArray *)namesOfEntitiesToExclude;
@end

@implementation NSManagedObject (Clone)

- (NSManagedObject *)cloneInContext:(NSManagedObjectContext *)context
                    withCopiedCache:(NSMutableDictionary **)alreadyCopied
                    excludeEntities:(NSArray *)namesOfEntitiesToExclude {
    if (!context) {
        CD_CUSTOM_DEBUG_LOG(@"%@:%@ Try to clone NSManagedObject in the 'nil' context.",
                            THIS_CLASS,
                            THIS_METHOD);
        return nil;
    }
    NSString *entityName = [[self entity] name];

    if ([namesOfEntitiesToExclude containsObject:entityName]) {
        return nil;
    }
    NSManagedObject *cloned = nil;
    if (alreadyCopied != NULL) {
        cloned = [*alreadyCopied objectForKey:[self objectID]];
        if (cloned) {
            return cloned;
        }
        // Create new object in data store
        cloned = [NSEntityDescription insertNewObjectForEntityForName:entityName
                                               inManagedObjectContext:context];
        [*alreadyCopied setObject:cloned forKey:[self objectID]];
    } else {
        CD_CUSTOM_DEBUG_LOG(@"%@:%@ NULL pointer was passed in 'alreadyCopied' argument.",
                            THIS_CLASS,
                            THIS_METHOD);
    }
    // Loop through all attributes and assign then to the clone
    NSDictionary *attributes = [[NSEntityDescription entityForName:entityName
                                            inManagedObjectContext:context] attributesByName];
    for (NSString *attr in attributes) {
        [cloned setValue:[self valueForKey:attr] forKey:attr];
    }

    // Loop through all relationships, and clone them.
    NSDictionary *relationships = [[NSEntityDescription entityForName:entityName
                                               inManagedObjectContext:context] relationshipsByName];
    NSArray *relationshipKeys = [relationships allKeys];
    for (NSString *relName in relationshipKeys) {
        NSRelationshipDescription *rel = [relationships objectForKey:relName];
        NSString *keyName = [rel name];
        if ([rel isToMany]) {
            if ([rel isOrdered]) {
                // Get a set of all objects in the relationship
                NSMutableOrderedSet *sourceSet = [self mutableOrderedSetValueForKey:keyName];
                NSMutableOrderedSet *clonedSet = [cloned mutableOrderedSetValueForKey:keyName];
                for (id relatedObject in sourceSet) {
                    //Clone it, and add clone to set
                    NSManagedObject *clonedRelatedObject = [relatedObject cloneInContext:context
                                                                         withCopiedCache:alreadyCopied
                                                                         excludeEntities:namesOfEntitiesToExclude];
                    if (clonedRelatedObject) {
                        [clonedSet addObject:clonedRelatedObject];
                    }
                }
            } else {
                // Get a set of all objects in the relationship
                NSMutableSet *sourceSet = [self mutableSetValueForKey:keyName];
                NSMutableSet *clonedSet = [cloned mutableSetValueForKey:keyName];
                for (id relatedObject in sourceSet) {
                    //Clone it, and add clone to set
                    NSManagedObject *clonedRelatedObject = [relatedObject cloneInContext:context
                                                                         withCopiedCache:alreadyCopied
                                                                         excludeEntities:namesOfEntitiesToExclude];
                    if (clonedRelatedObject) {
                        [clonedSet addObject:clonedRelatedObject];
                    }
                }
            }
        } else {
            NSManagedObject *relatedObject = [self valueForKey:keyName];
            if (relatedObject) {
                NSManagedObject *clonedRelatedObject = [relatedObject cloneInContext:context
                                                                     withCopiedCache:alreadyCopied
                                                                     excludeEntities:namesOfEntitiesToExclude];
                [cloned setValue:clonedRelatedObject forKey:keyName];
            }
        }
    }
    return cloned;
}

- (NSManagedObject *)cloneInContext:(NSManagedObjectContext *)context
                    excludeEntities:(NSArray *)namesOfEntitiesToExclude {
    NSMutableDictionary* mutableDictionary = [NSMutableDictionary dictionary];
    return [self cloneInContext:context
                withCopiedCache:&mutableDictionary
                excludeEntities:namesOfEntitiesToExclude];
}

@end
Nary answered 3/3, 2012 at 12:42 Comment(4)
This doesn't seem to actually preserve the order of the relationships correctly. I'll let you know if I find a fix. It seems like the order ends up backwards, but it just may be indeterminate.Lac
Are you sure? It worked for me and also I cannot locate a problem in code fragment I put as answer.Nary
Yea, the order is indeterminate. I have a "Page" with an ordered relationship of "strokes". When I clone the page I get all the stroke relationships, but they are out of order.Lac
I always get a reverse ordering. Have you found a fix, perchance?Lotty
O
5

I had a real need to work around the mass copying issue that @derrick acknowledged in his original answer. I modified from MasonK's version. This doesn't have a lot of the elegance that was in the previous versions; but it appears to solve a key issue (unintended duplicates of similar entities) in my application.

//
//  NSManagedObject+Clone.h

#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>

@interface NSManagedObject (Clone) {
}

-(NSManagedObject *)cloneInContext:(NSManagedObjectContext *)context withCopiedCache:(NSMutableDictionary *)alreadyCopied exludeEntities:(NSMutableArray *)namesOfEntitiesToExclude  isFirstPass:(BOOL)firstPass;

-(NSManagedObject *)cloneInContext:(NSManagedObjectContext *)context exludeEntities:(NSMutableArray *)namesOfEntitiesToExclude;

-(NSManagedObject *) clone;

@end

//
//  NSManagedObject+Clone.m
//

#import "NSManagedObject+Clone.h"

@implementation NSManagedObject (Clone)

-(NSManagedObject *) clone {
    NSMutableArray *emptyArray = [NSMutableArray arrayWithCapacity:1];
    return [self cloneInContext:[self managedObjectContext] exludeEntities:emptyArray];
}


- (NSManagedObject *)cloneInContext:(NSManagedObjectContext *)context withCopiedCache:(NSMutableDictionary *)alreadyCopied exludeEntities:(NSMutableArray *)namesOfEntitiesToExclude
isFirstPass:(BOOL)firstPass
{
    NSString *entityName = [[self entity] name];

    if ([namesOfEntitiesToExclude containsObject:entityName]) {
        return nil;
    }

    NSManagedObject *cloned = [alreadyCopied objectForKey:[self objectID]];
    if (cloned != nil) {
        return cloned;
    }

    //create new object in data store
    cloned = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:context];
    [alreadyCopied setObject:cloned forKey:[self objectID]];

    //loop through all attributes and assign then to the clone
    NSDictionary *attributes = [[NSEntityDescription entityForName:entityName inManagedObjectContext:context] attributesByName];

    for (NSString *attr in attributes) {
        [cloned setValue:[self valueForKey:attr] forKey:attr];
    }

    //Inverse relationships can cause all of the entities under one area to get duplicated
    //This is the reason for "isFirstPass" and "excludeEntities"

    if (firstPass == TRUE) {
        [namesOfEntitiesToExclude addObject:entityName];
        firstPass=FALSE;
    }

    //Loop through all relationships, and clone them.
    NSDictionary *relationships = [[NSEntityDescription entityForName:entityName inManagedObjectContext:context] relationshipsByName];
    for (NSString *relName in [relationships allKeys]){
        NSRelationshipDescription *rel = [relationships objectForKey:relName];

        NSString *keyName = rel.name;
        if ([rel isToMany]) {
            if ([rel isOrdered]) {
                NSMutableOrderedSet *sourceSet = [self mutableOrderedSetValueForKey:keyName];
                NSMutableOrderedSet *clonedSet = [cloned mutableOrderedSetValueForKey:keyName];

                NSEnumerator *e = [sourceSet objectEnumerator];

                NSManagedObject *relatedObject;
                while ( relatedObject = [e nextObject]){
                    //Clone it, and add clone to set
                    NSManagedObject *clonedRelatedObject = [relatedObject cloneInContext:context withCopiedCache:alreadyCopied exludeEntities:namesOfEntitiesToExclude
                                                            isFirstPass:firstPass];

                    if (clonedRelatedObject != nil) {
                    [clonedSet addObject:clonedRelatedObject];
                    [clonedSet addObject:clonedRelatedObject];
                    }
                }
            }
            else {
                NSMutableSet *sourceSet = [self mutableSetValueForKey:keyName];
                NSMutableSet *clonedSet = [cloned mutableSetValueForKey:keyName];
                NSEnumerator *e = [sourceSet objectEnumerator];
                NSManagedObject *relatedObject;
                while ( relatedObject = [e nextObject]){
                    //Clone it, and add clone to set
                    NSManagedObject *clonedRelatedObject = [relatedObject cloneInContext:context withCopiedCache:alreadyCopied exludeEntities:namesOfEntitiesToExclude
                                                                             isFirstPass:firstPass];
                    if (clonedRelatedObject != nil) {
                    [clonedSet addObject:clonedRelatedObject];
                    }
                }
            }
        }
        else {
            NSManagedObject *relatedObject = [self valueForKey:keyName];
            if (relatedObject != nil) {
                NSManagedObject *clonedRelatedObject = [relatedObject cloneInContext:context withCopiedCache:alreadyCopied exludeEntities:namesOfEntitiesToExclude
                isFirstPass:firstPass];
                if (clonedRelatedObject != nil) {
                [cloned setValue:clonedRelatedObject forKey:keyName];
                }
            }
        }

    }

    return cloned;
}

-(NSManagedObject *)cloneInContext:(NSManagedObjectContext *)context exludeEntities:(NSMutableArray *)namesOfEntitiesToExclude {
    return [self cloneInContext:context withCopiedCache:[NSMutableDictionary dictionary] exludeEntities:namesOfEntitiesToExclude isFirstPass:TRUE];
}
@end
Overmeasure answered 24/7, 2013 at 4:10 Comment(2)
Yes, exluding entities didn't work with MasonK's version! Thanks JustinIncrease
Pay attention - in some place you have duplicate code line : [clonedSet addObject:clonedRelatedObject]; It may not have effect since clonedSet is an NSSet - still its not desirable.Keratitis
E
4

What you are asking for is called a "deep copy". Because it can be very expensive (as in unbounded memory usage) and very difficult to get right (consider loops in the object graph), Core Data does not provide this facility for you.

There is often an architecture that avoids the need however. Instead of making a copy of an entire object graph, perhaps you can create a new entity that encapsulates the differences (or future differences) that you would have if you copied the object graph and then references the original graph only. In other words, instantiate a new "customizer" entity and don't copy the entire object graph. For example, consider a set of row houses. Each has identical framing and appliances, but the owner can customize the paint and furniture. Instead of deep copying the entire house graph for each owner, have a "painting and furniture" entity—that references the owner and the house model—for each owner.

Ectoderm answered 28/4, 2010 at 16:53 Comment(0)
M
4

Here you have my swift 3 approach:

func shallowCopy(copyRelations: Bool) -> NSManagedObject? {

        guard let context = managedObjectContext, let entityName = entity.name else { return nil }
        let copy = NSEntityDescription.insertNewObject(forEntityName: entityName, into: context)
        let attributes = entity.attributesByName
        for (attrKey, _) in attributes {
            copy.setValue(value(forKey: attrKey), forKey: attrKey)
        }

        if copyRelations {
            let relations = entity.relationshipsByName

            for (relKey, relValue) in relations {
                if relValue.isToMany {
                    let sourceSet = mutableSetValue(forKey: relKey)
                    let clonedSet = copy.mutableSetValue(forKey: relKey)
                    let enumerator = sourceSet.objectEnumerator()

                    while let relatedObject = enumerator.nextObject()  {
                        let clonedRelatedObject = (relatedObject as! NSManagedObject).shallowCopy(copyRelations: false)
                        clonedSet.add(clonedRelatedObject!)
                    }
                } else {
                    copy.setValue(value(forKey: relKey), forKey: relKey)
                }
            }
        }

        return copy
    }
Murmansk answered 15/5, 2017 at 16:31 Comment(6)
copyRelations Bool is used to avoid copying inverse relations in our objectMurmansk
How come you are calling this a shallow copy?Torritorricelli
What name do you propose? I called shallow because It isn't a complete copy. In the case the relationed entities has another relation (not only the bidirectional one) then, they won't be copied. That method works for some cases but you can modify it to your needsMurmansk
Ah that makes sense. Perfect name.Torritorricelli
This answer is great, and fits exactly MY needs, but is not answering to the original question - which was about duplicating a whole "tree" of inter-related NSManagedObject. I think other answers got it wrong too (because of back relations and other circular relations, and the possibility of "meeting" the same entity again and again via indirect relations - I do believe the OP didn't mean to make a duplicate of these evert time...Keratitis
question was not relevant to shallow copy.Tindall
O
2

This is called a "deep copy." Because it can be surprisingly expensive, a lot of languages/libraries don't support it out of the box and require you to roll your own. Cocoa is unfortunately one of them.

Osbert answered 28/4, 2010 at 15:33 Comment(1)
Yes. "...surprisingly expensive..." as in unbounded memory usage. There's no way for the library to do this automatically without letting you easily shoot yourself in the foot.Ectoderm
R
1

This is a slight improvement on the contributions of @Geoff H, @Derrick, @Dmitry Makarenko & @masonk. The code uses a more functional style and throws friendly errors 😉.

Feature list:

  • Handles both One-to-One & One-to-Many relationships
  • Handles ordered relationships
  • Copies the entire NSManagedObject graph (using an alreadyCopied cache)
  • Implemented as an extension to NSManagedObject
  • NEW: Throws a custom DeepCopyError instead of crashing with fatalError()
  • NEW: Optionally retrieves the NSManagedObjectContext from the NSManagedObject itself

Swift 5.0 code

WARNING: this code is only partially tested!

import CoreData

extension NSManagedObject {
    
    enum DeepCopyError: Error {
        case missingContext
        case missingEntityName(NSManagedObject)
        case unmanagedObject(Any)
    }
    
    func deepcopy(context: NSManagedObjectContext? = nil) throws -> NSManagedObject {
        
        if let context = context ?? managedObjectContext {
            
            var cache = Dictionary<NSManagedObjectID, NSManagedObject>()
            return try deepcopy(context: context, cache: &cache)
            
        } else {
            throw DeepCopyError.missingContext
        }
    }

    private func deepcopy(context: NSManagedObjectContext, cache alreadyCopied: inout Dictionary<NSManagedObjectID, NSManagedObject>) throws -> NSManagedObject {
                
        guard let entityName = self.entity.name else {
            throw DeepCopyError.missingEntityName(self)
        }
        
        if let storedCopy = alreadyCopied[self.objectID] {
            return storedCopy
        }

        let cloned = NSEntityDescription.insertNewObject(forEntityName: entityName, into: context)
        alreadyCopied[self.objectID] = cloned
        
        // Loop through all attributes and assign then to the clone
        NSEntityDescription
            .entity(forEntityName: entityName, in: context)?
            .attributesByName
            .forEach { attribute in
                cloned.setValue(value(forKey: attribute.key), forKey: attribute.key)
            }
        
        // Loop through all relationships, and clone them.
        try NSEntityDescription
            .entity(forEntityName: entityName, in: context)?
            .relationshipsByName
            .forEach { relation in
                
                if relation.value.isToMany {
                    if relation.value.isOrdered {
                        
                        // Get a set of all objects in the relationship
                        let sourceSet = mutableOrderedSetValue(forKey: relation.key)
                        let clonedSet = cloned.mutableOrderedSetValue(forKey: relation.key)
                        
                        for object in sourceSet.objectEnumerator() {
                            if let relatedObject = object as? NSManagedObject {
                                
                                // Clone it, and add clone to the set
                                let clonedRelatedObject = try relatedObject.deepcopy(context: context, cache: &alreadyCopied)
                                clonedSet.add(clonedRelatedObject as Any)
                                
                            } else {
                                throw DeepCopyError.unmanagedObject(object)
                            }
                        }
                        
                    } else {
                        
                        // Get a set of all objects in the relationship
                        let sourceSet = mutableSetValue(forKey: relation.key)
                        let clonedSet = cloned.mutableSetValue(forKey: relation.key)
                        
                        for object in sourceSet.objectEnumerator() {
                            if let relatedObject = object as? NSManagedObject {
                                
                                // Clone it, and add clone to the set
                                let clonedRelatedObject = try relatedObject.deepcopy(context: context, cache: &alreadyCopied)
                                clonedSet.add(clonedRelatedObject as Any)
                                
                            } else {
                                throw DeepCopyError.unmanagedObject(object)
                            }
                        }
                    }
                    
                } else if let relatedObject = self.value(forKey: relation.key) as? NSManagedObject {
                    
                    // Clone it, and assign then to the clone
                    let clonedRelatedObject = try relatedObject.deepcopy(context: context, cache: &alreadyCopied)
                    cloned.setValue(clonedRelatedObject, forKey: relation.key)
                    
                }
            }
        
        return cloned
    }
}
Riverhead answered 6/4, 2021 at 10:25 Comment(0)
M
0

Also:

[clone setValuesForKeysWithDictionary:[item dictionaryWithValuesForKeys:[properties allKeys]]];
[clone setValuesForKeysWithDictionary:[item dictionaryWithValuesForKeys:[attributes allKeys]]];
Martyry answered 31/8, 2010 at 13:25 Comment(1)
What is properties and attributes? How do you get them?Diversification
H
0

If you want to only relate Entities in the Relationship Hierarchie you only have to add the following Code to the solution of Dmitry

Between this

NSString *entityName = [[self entity] name];

HERE if ([namesOfEntitiesToExclude containsObject:entityName]) {

NSMutableArray *arrayToOnlyRelate   = [NSMutableArray arrayWithObjects:@"ENTITY 1",@"ENTITY 2",@"ENTITY 3", nil];

if ([arrayToOnlyRelate containsObject:entityName]) {
    return self;
}
Heartland answered 20/2, 2015 at 10:1 Comment(0)
I
0

My take on this is at https://gist.github.com/jpmhouston/7958fceae9216f69178d4719a3492577

  • passes rel.inverseRelationship.name into the recursive method to omit visiting the inverse relationships rather than maintaining a set of alreadyCopied objects

  • shallow or deep copies

  • accepts keypaths of relationships to not clone, but to either omit, or to simply copy if the inverse is a to-many relationship

  • workaround for ordered,to-many relationships ending up in backwards order - simply iterate over the source entities backwards :) i'm not sure if this a good idea or if it even works all the time

Feedback & comments welcome, especially if someone can elaborate on Benjohn's comment on mis-ordering above "The work around for this is to build the complete ordered set and then assign using the primitive KVO variant." and can improve my ordered,to-many workaround.

Also, I'm using MagicalRecord and so my code assumes that, including providing easy methods that use its default context.

Isosceles answered 31/5, 2016 at 17:57 Comment(0)
T
0

Swift 4.0 version

import UIKit
import CoreData

class ManagedObjectCloner: NSObject {

    static func cloneObject(source :NSManagedObject, context :NSManagedObjectContext) -> NSManagedObject{
        let entityName = source.entity.name
        let cloned = NSEntityDescription.insertNewObject(forEntityName: entityName!, into: context)

        let attributes = NSEntityDescription.entity(forEntityName: entityName!, in: context)?.attributesByName

        for (key,_) in attributes! {
            cloned.setValue(source.value(forKey: key), forKey: key)
        }

        let relationships = NSEntityDescription.entity(forEntityName: entityName!, in: context)?.relationshipsByName
        for (key,_) in relationships! {
            let sourceSet = source.mutableSetValue(forKey: key)
            let clonedSet = cloned.mutableSetValue(forKey: key)
            let e = sourceSet.objectEnumerator()

            var relatedObj = e.nextObject() as? NSManagedObject

            while ((relatedObj) != nil) {
                let clonedRelatedObject = ManagedObjectCloner.cloneObject(source: relatedObj!, context: context)
                clonedSet.add(clonedRelatedObject)
                relatedObj = e.nextObject() as? NSManagedObject
            }
        }

        return cloned
    }

}
Tindall answered 26/7, 2018 at 17:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.