How can I duplicate, or copy a Core Data Managed Object?
Solution 1
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
Solution 2
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
Solution 3
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;
}
Solution 4
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
Solution 5
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
.
import CoreData
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)
Mani
Updated on April 07, 2021Comments
-
Mani about 3 years
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!
-
Barry Wark about 14 yearsYes. "...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.
-
Barry Wark about 14 yearsThis doesn't do a "deep copy" of relationships that the OP wants.
-
Jaanus about 14 yearsYep, 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.
-
Joe D'Andrea almost 14 yearsThis 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.
-
levous about 13 yearsthis 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"
-
Mustafa about 13 yearsI'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]>
-
Mustafa about 13 yearsNote: 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.
-
Z S almost 13 yearsOne 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.
-
Nathan Gaskin over 12 years@Mustafa 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.
-
Duane Fields almost 12 yearsThis 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.
-
Dmitry Makarenko almost 12 yearsAre you sure? It worked for me and also I cannot locate a problem in code fragment I put as answer.
-
Duane Fields almost 12 yearsYea, 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.
-
RyeMAC3 over 11 yearsSo 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.
-
Felix Lamouroux over 11 years
[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. -
Christoph over 11 yearsNSString *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 relation
-
Jeff about 11 yearsThis 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 withexcludeEntities
to follow Apple naming guidelines of ever lengthening method names. And fixed your "exlude" misspelling. -
Alex Stone about 11 yearsDoes not work for my RestKit project in iOS6 with ARC. I get EXC_BAD_ACCESS at NSEnumerator *e = [sourceSet objectEnumerator];
-
Alex Stone about 11 yearsThis code works in iOS6 with ARC for nested relationship and using RestKit! Thanks
-
masonk almost 11 yearsThis 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.
-
Diego Barros almost 11 yearsThis is the most complete implementation here.
-
Joe over 10 yearsThis 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.
-
Mark Bridges over 10 yearsI'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''
-
Mark Bridges over 10 yearsHad a crash trying to use Derrick's answer but this one works for me.
-
MGM about 10 yearsThere are lots of different answers here, but this one worked with objects that had attributes that were both toone and tomany.
-
Aqib Mumtaz over 9 yearsThis is great answer, must be promoted!
-
elsurudo almost 9 yearsThis 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? -
elsurudo almost 9 yearsI always get a reverse ordering. Have you found a fix, perchance?
-
Damien Romito over 8 yearsYes, exluding entities didn't work with MasonK's version! Thanks Justin
-
Benjohn over 8 yearsI 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. -
anoop4real about 8 years@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.
-
Aviram Netanel about 8 yearsit worked! but when I change the source to nil, the clone is also changing! how is that possible?!?!
-
koen about 8 years@elsurudo: I think that is a typo.
-
kelin about 7 yearsWhat is
properties
andattributes
? How do you get them? -
Roberto Frontado about 7 yearsBad typo then ;)
-
Bisca about 7 yearscopyRelations Bool is used to avoid copying inverse relations in our object
-
Zia almost 7 yearsHow come you are calling this a shallow copy?
-
Bisca almost 7 yearsWhat 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 needs
-
Zia almost 7 yearsAh that makes sense. Perfect name.
-
Yoon Lee over 6 yearsThis 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.
-
Motti Shneor over 6 yearsThis 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...
-
Motti Shneor over 6 yearsPay 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.
-
Aaban Tariq Murtaza almost 6 yearsquestion was not relevant to shallow copy.
-
Aaban Tariq Murtaza almost 6 yearswho asked for shallow copy?
-
Rillieux over 3 yearsCould you clarify something - does this "clone" automatically get added to the database, or is it in "limbo" until is the context is saved?
-
ngb over 2 yearsthere 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.
-
ReDetection over 2 yearsexcuse 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 nowadays