如何将NSManagedObject从一个上下文复制或移动到另一个上下文?

57

我假设我的设置是相当标准的,有一个刮擦板MOC(包含从网上下载的一堆对象),它从不保存,和另一个持久化MOC,它保留对象。当用户从scratchMOC中选择一个对象添加到她的库中时,我想要么1)从scratchMOC中删除该对象并插入permanentMOC中,要么2)将该对象复制到permanentMOC中。Core Data FAQ说我可以像这样复制一个对象:

NSManagedObjectID *objectID = [managedObject objectID];
NSManagedObject *copy = [context2 objectWithID:objectID];

在这种情况下,context2将是permanentMOC。但是,当我这样做时,复制的对象会出现错误;数据最初未解析。当它稍后得到解析时,所有值都为nil;没有从原始managedObject实际复制或引用任何数据(属性或关系)。因此,我看不出使用这个objectWithID:方法和只是使用insertNewObjectForEntityForName:将完全新的对象插入permanentMOC之间有任何区别。
我意识到我可以在permanentMOC中创建一个新对象,并手动复制旧对象的每个键值对,但我对这个解决方案不太满意。(我有许多不同的托管对象都有这个问题,因此我不想在继续开发时编写和更新所有这些对象的copy:方法。)有更好的方法吗?
5个回答

57

首先,在单个线程上拥有多个NSManagedObjectContext不是标准配置。99%的情况下,您只需要一个上下文,这将为您解决此问题。

为什么您觉得需要多个NSManagedObjectContext

更新

实际上,这是我见过的少数几种有意义的用例之一。要做到这一点,您需要对对象从一个上下文递归复制到另一个上下文。工作流程如下:

  1. 在持久性上下文中创建新对象
  2. 从源对象获取属性的字典(使用 -dictionaryWithValuesForKeys -[NSEntityDescription attributesByName]来完成此操作。
  3. 将值字典设置到目标对象上(使用-setValuesForKeysWithDictionary
  4. 如果您有关系,则需要递归执行此副本并遍历关系,可以使用硬编码(以避免某些循环逻辑)或使用-[NSEntityDescription relationshipsByName]

正如其他人所提到的,您可以从The Pragmatic Programmers Core Data Book下载我的书中的示例代码,并查看此问题的一个解决方案。当然,在书中我会更深入地讨论它 :)


4
我的应用程序有一个地图视图(mapView),在启动应用程序时(或用户更改位置)会下载数百或数千个对象作为注释(annotation)使用。我的想法是,将地图对象保存在第二个MOC(scratchMOC)中会更加优雅和高效,scratchMOC不会被保存; 根据设计,它会在每次应用程序启动时被删除并重新加载。然后,用户可以选择一个或多个地图对象添加到他的库中,这些对象将被复制/移动到永久化的MOC中。我认为这可以避免迭代通过MOC删除数千个地图对象以退出应用程序的过程。 - Aeonaut
7
由于某种原因,我在执行第二步时遇到了困难,所以这里是正确的代码:NSDictionary *newValues = [oldObject dictionaryWithValuesForKeys:[[objectEntity attributesByName] allKeys]]。该代码旨在获取旧对象的所有属性值,并将其存储在一个新的NSDictionary对象中。 - samvermette
5
@Marcus,随着CoreData的改进(例如现在有父子MOC),您的意见仍然有效吗?因为现在我觉得即使是像创建新对象然后选择取消/保存这样的简单情况,使用多个MOC也不仅可以接受,而且是推荐的。苹果的示例CoreDataBooks现在就是使用多个MOC来实现此功能的。 - Rhubarb
使用新的父/子MOC,特别是在创建新对象并希望能够轻松取消时,在单线程情况下使用它们开始变得有意义。@Rhubarb - Marcus S. Zarra
2
"在单个线程上使用NSManagedObjectContext并不是标准做法" -- 这完全不正确。 - TheCodingArt
显示剩余6条评论

10

文档存在误导和不完整的情况。objectID方法本身并不会复制对象,它们只是保证你获得了想要的特定对象。

在示例中,context2实际上是源上下文而不是目标上下文。由于目标上下文中不存在该ID对应的对象,所以你得到了nil。

由于对象图的复杂性以及上下文管理对象图的方式,复制受管理的对象相当复杂。你必须在新的上下文中详细地重新创建复制的对象。

这里有一些示例代码,我从Core Data: Apple's API for Persisting Data on Mac OS X《务实程序员》的示例代码中剪切而来(你可能可以在Pragmatic网站下载整个项目代码而不用购买书籍)。它应该给你提供了如何在上下文之间复制对象的大致思路。

你可以创建一些基础代码来复制对象,但每个对象图的关系具体情况通常意味着你需要为每个数据模型进行定制。


1
我喜欢你的示例代码,但是我无法弄清楚“self”是什么。copyObject是要复制的对象。toContext是目标上下文。parentEntity是超类。但是[self lookup]是什么类?这段代码在哪里? - Martin Algesten
好的,我明白了。lookup只是一个字典,用于在遍历对象图时避免重复使用相同的对象。 - Martin Algesten
首先,感谢您提供的代码片段,非常棒!当我在一个示例(事件和设备模型,事件具有creatingDevice,设备具有事件)上使用此代码时,我获取了所有事件,然后调用了您的函数,然后在新的上下文中调用save(),但它抛出了一个异常,说creatingDevice是nil...我做错了什么?谢谢! - kennyevo

7
我曾经遇到同样的问题,找到了这篇关于创建断开连接实体并稍后添加到上下文中的文章:http://locassa.com/temporary-storage-in-apples-coredata/ 思路是你有一个NSManagedObject,因为你将要把对象存储在数据库中。我的难点是许多这些对象是通过HTTP API下载的,我想在会话结束时丢掉大部分对象。比如用户帖子流,我只想保存那些被标记为收藏或草稿的帖子。
我使用下面的方式创建所有的帖子:
+ (id)newPost {
    NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"Post" inManagedObjectContext:self.managedObjectContext];
    Post *post = [[Post alloc] initWithEntity:entityDescription insertIntoManagedObjectContext:nil];
    return post;
}

当帖子被收藏时,它们会被插入到本地托管对象上下文中

+ (BOOL)favoritePost:(Post *)post isFavorite:(BOOL)isFavorite
{
    // Set the post's isFavorite flag
    post.isFavorite = [NSNumber numberWithBool:isFavorite];

    // If the post is being favorited and not yet in the local database, add it
    NSError *error;
    if (isFavorite && [self.managedObjectContext existingObjectWithID:post.objectID error:&error] == nil) {
        [self.managedObjectContext insertObject:post];
    }
    // Else if the post is being un-favorited and is in the local database, delete it
    else if (!isFavorite && [self.managedObjectContext existingObjectWithID:post.objectID error:&error] != nil) {
        [self.managedObjectContext deleteObject:post];
    }

    // If there was an error, output and return NO to indicate a failure
    if (error) {
        NSLog(@"error: %@", error);
        return NO;
    }

    return YES;
}

希望这能帮到您。

关于'insertIntoManagedObjectContext:nil'的注意事项:这似乎在iOS8中已被弃用。 - Mike M

1

你需要确保保存了managedObject所在的上下文。为了在不同的上下文中获取相同的对象,它需要存在于持久存储中。

根据文档objectWithID:始终返回一个对象。因此,故障解析为对象的所有nil值意味着它无法在持久存储中找到您的对象。


1
刚刚确认这个功能确实可以正常工作 - 谢谢!然而,我最初想要第二个上下文(scratchMOC)的原因实际上是为了避免保存任何对象;当用户退出并重新启动应用程序时,我希望所有内容都被丢弃并重新加载。猜想这可能不可能实现。 - Aeonaut
看一下内存存储类型。它是一种只存在于内存中的存储,当程序退出时就会消失。只要小心,你不能在不同的存储之间建立关系。 - Alex
Alex,看起来非常诱人,但我有点难以理解它是如何工作的。这两个存储(持久性和内存中)是在两个独立的上下文中还是在同一个上下文中? - Aeonaut

1

Swift 5

如果你想知道如何在2020年复制NSManagedObjects,下面的代码对我有效:

// `Restaurant` is the name of my managed object subclass.

// I like to have Xcode auto generate my subclasses (Codegen 
//     set to "Class Definition") & then just extend them with 
//     whatever functionality I need.

extension Restaurant {
    public func copy() -> Restaurant? {
        let attributes = entity.attributesByName.map { $0.key }
        let dictionary = dictionaryWithValues(forKeys: attributes)
        guard let context = AppDelegate.shared?.persistentContainer.viewContext,
            let restaurantCopy = NSEntityDescription.insertNewObject(forEntityName: Restaurant.entityName, into: context) as? Restaurant
            else
        {
            return nil
        }
        restaurantCopy.setValuesForKeys(dictionary)

        return restaurantCopy
    }
}

1
关系在哪里? - Alex

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