public
Last active

Copy and deep copy for NSManagedObject graphs.

  • Download Gist
NSManagedObject+DeepCopying.m
Objective-C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
/*
This makes some assumptions about your code, for example that you’ve got ownership rules set up properly. If a relationship to another entity uses the cascade deletion rule, it’s considered to be owned by the receiver, and thus will be copied by -deepCopyWithZone:.
*/
 
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
 
@interface NSManagedObject (RXCopying) <NSCopying>
 
-(void)setRelationshipsToObjectsByIDs:(id)objects;
 
-(id)deepCopyWithZone:(NSZone *)zone;
-(NSDictionary *)ownedIDs;
 
@end
 
 
@implementation NSManagedObject (RXCopying)
 
// copying
-(id)deepCopyWithZone:(NSZone *)zone {
NSMutableDictionary *ownedIDs = [[self ownedIDs] mutableCopy];
NSManagedObjectContext *context = [self managedObjectContext];
id copied = [self copyWithZone: zone]; // -copyWithZone: copies the attributes for us
 
for(NSManagedObjectID *key in [ownedIDs allKeys]) { // deep copy relationships
id copiedObject = [[context objectWithID: key] copyWithZone: zone];
[ownedIDs setObject: copiedObject forKey: key];
[copiedObject release];
}
 
[copied setRelationshipsToObjectsByIDs: ownedIDs];
for(NSManagedObjectID *key in [ownedIDs allKeys]) {
[[ownedIDs objectForKey: key] setRelationshipsToObjectsByIDs: ownedIDs];
}
return copied;
}
 
-(id)copyWithZone:(NSZone *)zone { // shallow copy
NSManagedObjectContext *context = [self managedObjectContext];
id copied = [[[self class] allocWithZone: zone] initWithEntity: [self entity] insertIntoManagedObjectContext: context];
 
for(NSString *key in [[[self entity] attributesByName] allKeys]) {
[copied setValue: [self valueForKey: key] forKey: key];
}
 
for(NSString *key in [[[self entity] relationshipsByName] allKeys]) {
[copied setValue: [self valueForKey: key] forKey: key];
}
return copied;
}
 
-(void)setRelationshipsToObjectsByIDs:(id)objects {
id newReference = nil;
NSDictionary *relationships = [[self entity] relationshipsByName];
for(NSString *key in [relationships allKeys]) {
if([[relationships objectForKey: key] isToMany]) {
id references = [NSMutableSet set];
for(id reference in [self valueForKey: key]) {
if(newReference = [objects objectForKey: [reference objectID]]) {
[references addObject: newReference];
} else {
[references addObject: reference];
}
}
[self setValue: references forKey: key];
} else {
if(newReference = [objects objectForKey: [[self valueForKey: key] objectID]]) {
[self setValue: newReference forKey: key];
}
}
}
}
 
-(NSDictionary *)ownedIDs {
NSDictionary *relationships = [[self entity] relationshipsByName];
NSMutableDictionary *ownedIDs = [NSMutableDictionary dictionary];
for(NSString *key in [relationships allKeys]) {
id relationship = [relationships objectForKey: key];
if([relationship deleteRule] == NSCascadeDeleteRule) { // ownership
if([relationship isToMany]) {
for(id child in [self valueForKey: key]) {
[ownedIDs setObject: child forKey: [child objectID]];
[ownedIDs addEntriesFromDictionary: [child ownedIDs]];
}
} else {
id reference = [self valueForKey: key];
[ownedIDs setObject: reference forKey: [reference objectID]];
}
}
}
return ownedIDs;
}
 
@end

This was incredibly helpful. Thanks so much for sharing.

Unless I'm mistaken, line 29 should be:

[copiedObject release];

Otherwise I believe you're leaking the copiedObject. -- Dan

You’re exactly right, Dan. Thanks for the catch. Update: Fixed!

really useful! thanks

Glad I could help :)

I know this is a few years old, but shouldn't line 32 be 'copied' rather than 'self'. Otherwise self is getting updated in the copy.

Good catch! This is what you get when you don’t unit test, folks: bugs that live for years.

@robrix Thanks for sharing :)
How should I call it on instance? [managedObject copy];?

I think you should changed the = to == in line 60 and line 68.

Using this category makes relationships to vanish from the 'from' object:

This is how I make the call

newDeepCopiedMO = [oldMO deepCopyWithZone:NULL];

child relationships in oldMO are successfully deep-copied to newDeepCopiedMO but removed from oldMO. Furthermore, changing attributes in newDeepCopiedMO after the copy changes them on oldMO as if it were the same object.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.