【问题标题】:How can I duplicate, or copy a Core Data Managed Object?如何复制或复制核心数据托管对象?
【发布时间】:2011-02-13 10:07:31
【问题描述】:

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

更具体地说,“B”(或其子项)包含的任何关系都不应该指向与“A”相关的对象。应该有一个全新的对象图,具有完整的相似关系,并且所有对象都具有相同的属性,但当然是不同的 id。

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

TIA!

【问题讨论】:

  • 这太棒了,一切都很好,但现在这里有 13 个相同算法的不同分支,每个分支都有自己有趣的功能和修复! … 所以。也许应该为每个问题提供一个 git repo 来解决这个问题:-)
  • 我认为没有“明显的手动”方法可以做到这一点,因为 NSManagedObject 是任意复杂关系图的一部分。您将需要一次又一次地处理循环关系、反向关系和“会议”相同的实体。这个问题映射到“复制子图”的问题。我对许多复杂的 CoreData 模型的经验是,您通常需要一个“浅”克隆器,它将克隆实体及其属性 - 并克隆它的关系。不是相关实体。含义 - 原版将与克隆版相同的实体相关。

标签: cocoa core-data nsmanagedobject


【解决方案1】:

这是我创建的一个类,用于执行托管对象的“深拷贝”:属性和关系。请注意,这不会检查对象图中的循环。 (感谢 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

【讨论】:

  • 这太棒了!我唯一的障碍: UITableView 并不总是在(实际上是新的)单元格中正确设置动画。我想知道它是否涉及 insertNewObjectForEntityForName:inManagedObjectContext:,然后执行深层复制,这可能会引发更多 NSFetchedResultsControllerDelegate 消息(?)。我没有任何循环,我复制的数据本身不是很深,所以希望这很好。也许有某种方法可以让我以某种方式“构建”整个克隆对象,然后一举将其插入,或者至少推迟发生添加的通知。看看这是否可行。
  • 这太棒了!但是,请注意。 'for (NSRelationshipDescription *rel in Relations)' 行不正确。关系是一个 NSDictionary。我已经修改以获取 NSRelationshipDescription 'for (NSString *relName in [relationships allKeys]){ NSRelationshipDescription *rel = [relationships objectForKey:relName]; ' 并检查“isToMany”
  • 我收到了这个错误。任何指针? *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSCFSet: 0x7003f80> was mutated while being enumerated.<CFBasicHash 0x7003f80 [0x1314400]>
  • @Mustafa 我知道那是几个月前的事了,但我在变异集上遇到了同样的问题。检查我的答案以获得可能的解决方案。
  • NSString *keyName = [NSString stringWithFormat:@"%@",rel] 在 iOS6 中不包含关系名称,使用 [NSString stringWithFormat:@"%@",rel.name]。否则 keyName 包含关系的整个描述字符串
【解决方案2】:

这些答案让我非常接近,尽管它们似乎确实有一些缺点:

第一,我听从了Z S的建议,把它放到了NSManagedObject上,这对我来说似乎更干净一些。

第二,我的对象图包含一对一的关系,所以我从 levous 的例子开始,但请注意,在一对一关系的情况下,levous 的例子并没有克隆对象。这将导致崩溃(尝试将 NSMO 从一个上下文保存到另一个上下文)。我已经在下面的示例中解决了这个问题。

第三,我提供了一个已经克隆的对象的缓存,这可以防止对象被克隆两次并因此在新的对象图中重复,也可以防止循环。

第四,我添加了一个黑名单(不克隆的实体类型列表)。我这样做的部分原因是为了解决我最终解决方案的一个缺点,我将在下面描述。

注意:如果您使用我理解的 CoreData 最佳实践,始终提供反向关系,那么这可能会克隆与您要克隆的对象有关系的 所有 对象。如果您正在使用逆,并且您有一个知道所有其他对象的根对象,那么您可能会克隆整个对象。我对此的解决方案是添加黑名单并传入我知道是我想要克隆的对象之一的父级的 Entity 类型。这似乎对我有用。 :)

克隆快乐!

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

【讨论】:

  • 这对我来说就像一个魅力。我有一对多和一对一的关系。我在层次结构中有不想克隆的对象。我的测试表明它运行良好。我欠你一杯啤酒。仅供参考,出于应用程序架构的原因,我确实从类别转换回类方法。我还用excludeEntities 交换了copiedCache 参数,以遵循Apple 命名准则,即不断延长方法名称。并修正了您的“排除”拼写错误。
  • 这段代码在 iOS6 中工作,ARC 用于嵌套关系并使用 RestKit!谢谢
  • 这是最接近工作的一个。但是,我的一些关系被定义为“有序”,这是 iOS 6 的新功能。所以我发布了我的修改来检查这一点。
  • 这里是最完整的实现。
  • 这是最好的解决方案,但有一个错误。在将 clonedRelatedObject 添加到 clonedSet 之前,您需要检查它是否为 nil。如果相关对象是 namesOfEntitiesToExclude 的一部分,它将为 nil 并在尝试将其添加到 clonedSet 时抛出错误。
【解决方案3】:

我已更新 user353759 的答案以支持 toOne 关系。

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

【讨论】:

  • 一个小的改进可能是使其成为 NSManagedObject 上的一个类别,因此您不必传入上下文和源对象。
  • 那么如果我有ObjectA和ObjectB,我可以复制ObjectA中的所有条目并将它们粘贴到ObjectB中吗?然后删除 ObjectA 并让我的所有条目都只包含 ObjectB 吗?这是“深拷贝”的目的吗?因为这就是我要找的。​​span>
  • 不适用于我在带有 ARC 的 iOS6 中的 RestKit 项目。我在 NSEnumerator *e = [sourceSet objectEnumerator]; 处得到 EXC_BAD_ACCESS
【解决方案4】:

这是@Derricks 的答案,经过修改以支持新的 iOS 6.0 订购对多关系,方法是询问关系以查看它是否已订购。当我在那里时,我为在同一个 NSManagedObjectContext 中进行克隆的常见情况添加了一个更简单的 -clone 方法。

//
//  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 的答案时发生了崩溃,但这对我有用。
  • 这个答案很好,必须推广!
  • 这通常有效,除了在我的情况下,每次复制时有序关系的顺序都会颠倒!有任何想法吗?另外,您是否有理由为有序关系两次 [clonedSet addObject:clonedRelatedObject];
  • 我认为这里的排序不太好。它似乎有一个我在自己的变体中修复的错误。对于具有反向的有序关系,深度优先克隆(在所有这些实现中都使用)可以递归地导致设置关系的另一端,这会在您构建有序集时更新它。 解决此问题的方法是构建完整的有序集,然后使用primitive KVO 变体进行分配。
  • @elsurudo:我认为这是一个错字。
【解决方案5】:

斯威夫特 5

这建立在 @Derrick 和 @Dmitry Makarenko 和 @masonk 的贡献之上,将所有内容整合在一起,对其进行改进并将其转变为适合 2020 年的解决方案。

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

    }

}

用法:

let myManagedObjectCopy = myManagedObject.copyEntireObjectGraph(context: myContext)

【讨论】:

  • 您能否澄清一下 - 这个“克隆”会自动添加到数据库中,还是在保存上下文之前处于“边缘”状态?
  • 代码中没有 managedObject.save() ,所以你必须自己保存。他的做法是正确的做法。
【解决方案6】:

我注意到当前答案存在一些错误。首先,在迭代时,某些东西似乎正在改变多对多相关对象的集合。其次,我不确定 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

【讨论】:

  • [cloned setValue:[source valueForKey:relName] forKey:relName]; 不会克隆一对一关系中的目标对象并导致异常,因为您可能会尝试链接来自不同上下文的对象。它也与 toMany 分支不一致。这可能是一个错字/轻微疏忽。
  • 这里有很多不同的答案,但这个适用于具有 toone 和 tomany 属性的对象。
【解决方案7】:

我已修改 Derrick's answer,它对我来说非常有效,以支持 iOS 5.0 和 Mac OS X 10.7 中可用的 ordered relationships

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

【讨论】:

  • 这似乎并没有真正正确地保留关系的顺序。如果我找到解决办法,我会告诉你的。看起来顺序倒退了,但它可能是不确定的。
  • 你确定吗?它对我有用,而且我无法在我作为答案的代码片段中找到问题。
  • 是的,订单不确定。我有一个具有“笔画”有序关系的“页面”。当我克隆页面时,我得到了所有的笔画关系,但它们是乱序的。
  • 我总是得到反向排序。你找到解决办法了吗?
【解决方案8】:

我确实需要解决 @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 的版本!谢谢贾斯汀
  • 注意 - 在某些地方你有重复的代码行:[clonedSet addObject:clonedRelatedObject];它可能没有效果,因为 clonedSet 是一个 NSSet - 仍然不可取。
【解决方案9】:

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

斯威夫特 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
    }
}

目标-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

这将返回一个包含所有属性且没有复制关系的克隆。

【讨论】:

  • 这不会对 OP 想要的关系进行“深拷贝”。
  • 是的,这只会复制属性。也可以做propertiesByName和/或relationshipByName,但我不会将它们添加到我的示例中,因为(与上面不同)我没有做过这种复制自己的关系并且不能保证它。
  • 这也没有副本,但对属性的引用存在值。简单地说,ObjA 的值被指向 ObjB,这意味着如果值被修改会影响 ObjA。
  • 谁要求浅拷贝?
【解决方案10】:

您要求的内容称为“深拷贝”。因为它可能非常昂贵(如无限的内存使用)并且很难正确处理(考虑对象图中的循环),Core Data 不为您提供此功能。

然而,通常有一种架构可以避免这种需要。除了复制整个对象图之外,也许您可​​以创建一个新实体来封装您在复制对象图然后仅引用原始图时会产生的差异(或未来差异)。换句话说,实例化一个新的“customizer”实体并且不要复制整个对象图。例如,考虑一组排屋。每个都有相同的框架和电器,但业主可以定制油漆和家具。与其为每个业主深度复制整个房屋图表,不如为每个业主提供一个“绘画和家具”实体——它引用业主和房屋模型。

【讨论】:

    【解决方案11】:

    这里有我的 swift 3 方法:

    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
        }
    

    【讨论】:

    • copyRelations Bool 用于避免在我们的对象中复制反向关系
    • 你怎么把这叫做浅拷贝?
    • 你建议什么名字?我称它为浅,因为它不是一个完整的副本。如果相关实体有另一个关系(不仅是双向关系),那么它们将不会被复制。该方法适用于某些情况,但您可以根据需要对其进行修改
    • 嗯,有道理。完美的名字。
    • 这个答案很棒,完全符合我的需求,但没有回答原始问题 - 这是关于复制相互关联的 NSManagedObject 的整个“树”。我认为其他答案也弄错了(因为反向关系和其他循环关系,以及通过间接关系一次又一次地“会见”同一实体的可能性 - 我确实相信 OP 并不是要复制这些永远...
    【解决方案12】:

    这称为“深拷贝”。因为它可能非常昂贵,所以许多语言/库不支持开箱即用,需要你自己动手。不幸的是,可可就是其中之一。

    【讨论】:

    • 是的。 “......令人惊讶的昂贵......”就像无限的内存使用一样。图书馆没有办法自动执行此操作,而不会让您轻松地朝自己的脚开枪。
    【解决方案13】:

    还有:

    [clone setValuesForKeysWithDictionary:[item dictionaryWithValuesForKeys:[properties allKeys]]];
    [clone setValuesForKeysWithDictionary:[item dictionaryWithValuesForKeys:[attributes allKeys]]];
    

    【讨论】:

    • 什么是propertiesattributes?你如何得到它们?
    【解决方案14】:

    如果您只想关联关系层次结构中的实体,您只需将以下代码添加到 Dmitry 的解决方案中

    在这之间

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

    这里 if ([namesOfEntitiesToExclude containsObject:entityName]) {

    NSMutableArray *arrayToOnlyRelate   = [NSMutableArray arrayWithObjects:@"ENTITY 1",@"ENTITY 2",@"ENTITY 3", nil];
    
    if ([arrayToOnlyRelate containsObject:entityName]) {
        return self;
    }
    

    【讨论】:

      【解决方案15】:

      我对此的看法是https://gist.github.com/jpmhouston/7958fceae9216f69178d4719a3492577

      • rel.inverseRelationship.name 传递到递归方法中以省略访问反向关系而不是维护一组alreadyCopied 对象

      • 浅拷贝或深拷贝

      • 接受关系的 keypaths克隆,但要么省略,或者如果逆是一对多关系则简单地复制

      • 有序的多对多关系以倒序结束的解决方法 - 只需遍历源实体 backwards :) 我不确定这是否是个好主意,或者甚至一直有效

      欢迎反馈和 cmets,特别是如果有人可以详细说明 Benjohn 对错误排序的评论 above "解决此问题的方法是构建完整的有序集,然后使用原始 KVO 进行分配变体。”并且可以改进我的有序,多对多解决方法。

      另外,我正在使用 MagicalRecord,所以我的代码假设,包括提供使用其默认上下文的简单方法。

      【讨论】:

        【解决方案16】:

        Swift 4.0 版本

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

        【讨论】:

          【解决方案17】:

          这是对@Geoff H、@Derrick、@Dmitry Makarenko 和@masonk 的贡献的轻微改进。代码使用了更实用的风格并抛出友好的错误?。

          功能列表:

          • 处理一对一和一对多关系
          • 处理有序关系
          • 复制整个 NSManagedObject 图(使用 alreadyCopied 缓存)
          • 作为NSManagedObject 的扩展实现
          • 新: 抛出自定义 DeepCopyError 而不是与 fatalError() 崩溃
          • 新:可选择从NSManagedObject 本身检索NSManagedObjectContext

          Swift 5.0 代码

          警告:此代码仅经过部分测试!

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

          【讨论】:

            猜你喜欢
            • 2019-08-31
            • 2012-01-30
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2023-03-30
            • 1970-01-01
            • 2017-08-31
            相关资源
            最近更新 更多