How can I duplicate, or copy a Core Data Managed Object?
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!
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)