如何复制或复制Core Data托管对象?

67

我有一个托管对象(“A”),它包含各种属性和关系类型,它的关系也有自己的属性和关系。我想做的是“复制”或“复制”以“ A”为根的整个对象图,从而创建一个非常类似于“ A”的新对象“ B”。

更具体地说,“B”(或其子代)包含的所有关系都不应指向与“ A”相关的对象。应该有一个全新的对象图,其中包含相似的关系,所有对象具有相同的属性,但ID当然不同。

有一种显而易见的手动方法可以实现这一点,但我希望了解一种更简单的方法,这种方法并不完全明显在Core Data文档中。

TIA!


这很棒,但现在这里有13个不同的分支算法,每个都有自己的有趣特性和修复!...S.O.或许应该为每个问题提供一个git仓库来解决这个问题 :-) - Benjohn
我认为没有“明显的手动”方法来做到这一点,因为NSManagedObject是一个任意复杂关系图的一部分。您需要解决循环关系、反向关系以及一遍又一遍地“遇见”相同的实体。这个问题映射到“复制子图”的问题上。我的经验是,在所有复杂性的CoreData模型中,通常需要一个“浅层”克隆器,它将克隆实体及其属性,并克隆其关系,而不是相关实体。也就是说,原始实体将与克隆实体关联到相同的实体。 - Motti Shneor
17个回答

61

这是我创建的一个类,用于执行托管对象的“深度复制”:属性和关系。请注意,这不会检查对象图中的循环。(感谢Jaanus提供了起点...)

@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

1
太棒了!我唯一的问题是:UITableView并不总是能够正确地(有效地新建)动画化单元格。我想知道这是否与insertNewObjectForEntityForName:inManagedObjectContext:有关,然后执行深度复制,这可能会引发更多的NSFetchedResultsControllerDelegate消息(?)。我没有任何循环,而且我复制的数据本身并不是很深,所以希望这是好的。也许有一些方法可以让我“构建”整个克隆对象,然后一次性插入它,或者至少推迟通知添加已经发生。正在寻找可行性。 - Joe D'Andrea
3
太棒了!不过需要注意一点:行 'for (NSRelationshipDescription *rel in relationships)' 是不正确的,relationships 是一个NSDictionary。我已经进行了修改以获取 NSRelationshipDescription 'for (NSString *relName in [relationships allKeys]){ NSRelationshipDescription *rel = [relationships objectForKey:relName];}' 并检查了 "isToMany"。 - levous
1
@Mustafa 我知道这已经是几个月前的事了,但我也遇到了相同的突变集问题。请查看我的答案,可能会有解决方案。 - Nathan Gaskin
1
NSString *keyName = [NSString stringWithFormat:@"%@",rel] 在iOS6中不包含关系名称,请使用[NSString stringWithFormat:@"%@",rel.name]。否则,keyName将包含关系的整个描述字符串。 - Christoph
它起作用了!但是当我将源更改为nil时,克隆也在改变!这怎么可能?!? - Aviram Netanel
显示剩余3条评论

43
这些答案让我离目标很近,但它们似乎有一些缺陷:
第一,我采纳了Z S的建议,在NSManagedObject上建立了一个类别(category),这对我来说更加清晰。
第二,我的对象图包含到单关系,所以我从levous的示例开始,但请注意在到单关系的情况下levous的示例没有克隆对象。这将导致崩溃(尝试在不同上下文中保存一个NSMO)。我在下面的示例中解决了这个问题。
第三,我提供了一个已经克隆对象的缓存,这可以防止对象被克隆两次,因此在新对象图中会重复,并且还可以防止循环。
第四,我添加了一个黑名单(不需克隆的实体类型列表)。我这样做部分是为了解决我最终解决方案的一个缺陷,我将在下面描述。
注意:如果您使用CoreData最佳实践,始终提供反向关系(inverse relationships),那么这可能会克隆与要克隆对象相关联的所有对象。如果您使用反向关系,并且有一个单个根对象知道所有其他对象,则可能会克隆整个对象图。我的解决方法是添加黑名单并传递我知道是要克隆对象之一的父实体类型。这对我似乎有效。 :)
愉快的克隆!
// 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

1
这是最接近有效的一个。然而,我的某些关系被定义为“有序”的,在iOS 6中是新内容。因此,我发布了我的修改以进行检查。 - masonk
这是最完整的实现。 - Diego Barros
1
这是最好的解决方案,但有一个漏洞。在将它添加到clonedSet之前,您需要检查clonedRelatedObject是否为nil。如果相关对象是namesOfEntitiesToExclude的一部分,则它将为nil并在尝试将其添加到clonedSet时抛出错误。 - Joe
我尝试过这个,但是在这一行出现了错误。NSMutableSet *clonedSet = [cloned mutableSetValueForKey:keyName]; *** 由于未捕获的异常 'NSInvalidArgumentException',原因:实体“Form”的NSManagedObjects不支持属性“contains”的-mutableSetValueForKey: - Mark Bridges
@MarkBridges 我知道现在有点晚了,但是无论如何,我得到了这个错误,因为我有一个有序关系,所以我使用了 Dmitry Makarenko 的解决方案,其中有序关系也被处理了,它起作用了。 - anoop4real
显示剩余2条评论

24

我已更新 user353759 的答案,以支持一对一关系。

@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;
}

3
可以将其作为NSManagedObject的一个类别(category)来改进,这样就不需要传递上下文(context)和源对象(source object)。 - Z S
那么如果我有ObjectA和ObjectB,我可以复制ObjectA中的所有条目并将它们粘贴到ObjectB中吗?然后删除ObjectA,只保留具有所有条目的ObjectB?这就是“深拷贝”的目的吗?因为这正是我正在寻找的。 - RyeMAC3
我的RestKit项目在iOS6中使用ARC无法工作。我在[NSEnumerator *e = [sourceSet objectEnumerator];]处遇到了EXC_BAD_ACCESS错误。 - Alex Stone

16

这是@Derricks的答案,经过修改以支持iOS 6.0新版本引入的有序to-many关系,通过查询关系来确定其是否有序。在那里,我添加了一个更简单的-clone方法,用于在同一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

尝试使用Derrick的答案时遇到了崩溃,但这个答案对我有效。 - Mark Bridges
这是一个很棒的答案,一定要推广! - Aqib Mumtaz
4
通常情况下这个方法是有效的,但在我的情况下,每次复制后有序关系的顺序都会颠倒!你有什么想法吗?另外,对于有序关系,你为什么要两次执行 [clonedSet addObject:clonedRelatedObject]; - elsurudo
我认为这里的排序不太好用。它似乎有一个错误,我在自己的变体中修复了它。对于具有反向关系的有序关系,深度优先克隆(如所有这些实现中使用的)可能会递归地导致关系的另一端被设置,从而在构建有序集时更新有序集。解决此问题的方法是构建完整的有序集,然后使用“primitive”KVO变量进行分配。 - Benjohn
@elsurudo:我认为这是一个打字错误。 - koen
糟糕的错别字;) - Roberto Frontado

16

Swift 5

这是在 @Derrick、@Dmitry Makarenko 和 @masonk 的贡献基础上构建的,将所有内容整合在一起,改进并将其转化为适合2020年的解决方案。

  • 处理1对1和1对多关系
  • 处理有序关系
  • 复制整个NSManagedObject图(使用已经复制的缓存)
  • 作为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
    
    }

}

使用方法:

let myManagedObjectCopy = myManagedObject.copyEntireObjectGraph(context: myContext)

你能澄清一下吗 - 这个“克隆”会自动添加到数据库中,还是在上下文保存之前处于“悬空”状态? - Rillieux
代码中没有managedObject.save(),所以你必须自己保存。他的做法是正确的做法。 - ngb
有点老了,但你帮我省了很多功夫。我确认这个完美地运作。谢谢! - undefined

8

我注意到当前答案中存在一些错误。首先,在迭代过程中,某些东西似乎正在改变与之相关的ToMany对象集合。其次,我不确定API是否发生了更改,但是在获取那些相关对象时,使用NSRelationshipDescription的字符串表示形式作为键会引发异常。

我进行了一些微调,进行了一些基本测试,看起来可以工作。如果有人想进一步调查,那就太好了!

@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

2
[cloned setValue:[source valueForKey:relName] forKey:relName]; 这段代码不会克隆目标对象中的单向关系,因此可能导致异常,因为您可能尝试链接来自不同上下文的对象。这也与toMany分支不一致。这很可能是一个拼写错误/小疏忽。 - Felix Lamouroux
这里有很多不同的答案,但是这个可以处理既有toone又有tomany属性的对象。 - MGM

6

我修改了Derrick的答案,这对我非常有效,现在支持iOS 5.0和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

这似乎实际上没有正确地保留关系的顺序。如果我找到了解决方法,我会让你知道。它似乎是顺序颠倒了,但可能是不确定的。 - Duane Fields
你确定吗?这段代码对我来说是有效的,而且我在我回答中放置的代码片段中也找不到问题。 - Dmitry Makarenko
是的,顺序是不确定的。我有一个带有“笔画”有序关系的“页面”。当我克隆页面时,我得到了所有的笔画关系,但它们是无序的。 - Duane Fields
我总是得到相反的顺序。你有找到解决方法吗? - elsurudo

6

这样做可以吗?(未经测试)这将是您提到的“手动方式”,但它会自动与模型更改等同步,因此您不必手动输入所有属性名称。

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

这将会返回一个克隆对象,所有属性都会被复制而关联关系则不会。

这不会进行所需的“深拷贝”关系。 - Barry Wark
是的,这只复制属性。也可以使用propertiesByName和/或relationshipsByName,但我不会将它们添加到我的示例中,因为(与上面不同)我自己没有进行过这种关系复制,并且无法保证其可靠性。 - Jaanus
这并不是复制,而是将值引用到属性。简单来说,ObjA的值被指向ObjB,这意味着如果修改了该值,则会影响ObjA。 - Yoon Lee
谁要求浅复制? - Aaban Tariq Murtaza

5

我有一个真正的需要解决@derrick在他的原始答案中承认的大规模复制问题。我从MasonK的版本进行了修改。这个版本没有之前版本中的许多优雅之处,但它似乎解决了我的应用程序中一个关键问题(相似实体的意外重复)。

//
//  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

是的,使用MasonK版本时排除实体无效!感谢Justin。 - Damien Romito
1
请注意 - 在某些地方,您有重复的代码行:[clonedSet addObject:clonedRelatedObject]; 这可能没有效果,因为clonedSet是一个NSSet - 尽管如此,这并不理想。 - Motti Shneor

4
你所要求的被称为“深拷贝”。由于它可能非常昂贵(例如无限制的内存使用)并且非常难以做到正确(考虑对象图中的循环),因此Core Data不为您提供此功能。
通常有一种架构可以避免这种需要。而不是复制整个对象图,也许您可以创建一个新实体,封装您将在复制对象图时具有的差异(或未来差异),然后仅引用原始图。换句话说,实例化一个新的“自定义”实体,而不是复制整个对象图。例如,请考虑一组联排别墅。每个别墅都有相同的框架和电器,但业主可以定制油漆和家具。而不是深度复制每个业主的整个房屋图,为每个业主提供“油漆和家具”实体 - 它引用业主和房屋模型。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接