如何避免更改 NSBatchInsertRequest 中的 属性 值?

How to avoid changing property values in an NSBatchInsertRequest?

我有一个简单的核心数据实体 Story,我偶尔会使用来自网络调用的最新数据进行更新。这个网络调用有时会更新很多很多故事实例,所以我 运行 一个 NSBatchInsertRequest,如下所示。 (我使用批量插入的另一个原因是许多故事可能需要添加到持久存储中。)

问题是用户可能已经将 Story 标记为收藏夹。当他们这样做时,我在主线程上设置 story.isFavorite = true 并保存 viewContext.

但是,当批量插入发生时 它会覆盖 story.isFavorite,将其设置回 false,即使我使用的是 NSMergeByPropertyObjectTrumpMergePolicy 在批量插入和查看上下文中。我也没有在批量插入处理程序中触及 story.isFavorite,所以我不希望 属性 被覆盖。

我认为使用此合并策略进行批量插入的好处是可以避免先获取 + 然后手动更新已更改的属性 + 最后保存。避免更改 NSBatchInsertRequest 中的 属性 值的正确方法是什么?

故事

@objc(Story)
public class Story: NSManagedObject {

    @NSManaged public var title:       String?
    @NSManaged public var storyURL:    URL?
    @NSManaged public var updatedTime: Date?
    @NSManaged public var isFavorite:  Bool // <- the problem property
}

批量插入

container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.automaticallyMergesChangesFromParent = false

let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.parent = container.viewContext
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

context.perform {

    let batchInsert = NSBatchInsertRequest(entity: Story.entity(), managedObjectHandler: { managedObject in
        let story         = managedObject as! Story
        let storyResponse = downloadedStories[I]

        // Update story with latest response data BUT don't modify story.isFavorite.

        story.title       = storyResponse.title
        story.storyURL    = storyResponse.storyURL
        story.updatedTime = storyResponse.updatedTime
        
        // ...
    })

    let result = try context.execute(batchInsert) as? NSBatchInsertResult
    if let insertedIDs = result?.result as? [NSManagedObjectID] {
        // Merge changes into parent context. Skip save() because not needed for batch insert.
        NSManagedObjectContext.mergeChanges(fromRemoteContextSave: [NSInsertedObjectsKey: insertedIDs], into: [container.viewContext])
    }
}

编辑

Story 实体确实具有使用属性 storyURL 的唯一值约束。

Michael Tsai 回答后更新

通过使 Story 实体属性 isFavorite 成为 non-Optional 布尔值 而没有 默认值(虽然之前它被标记为可选我不确定它在这里有什么不同)并保持 Use Scalar Type 框被选中,我可以确认商店中现有的 objects 不会(根本)使用此批处理配置进行修改插入上下文。

context.persistentStoreCoordinator = container.persistentStoreCoordinator

// HOWEVER, observe that regardless of the merge policy below,
// setting `context.parent = container.viewContext` will also
// overwrite the store data!

context.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy 
// NSMergeByPropertyObjectTrumpMergePolicy ignores objects in the store
// (which have the same unique constraint value, here equal `storyURL`)
// and overwrites all properties.

// To confirm that the batch insert operation does not modify
// existing Story instances (at all), first delete all instances where
// where isFavorite == false. Then load the all story data again and
// execute the NSBatchInsertRequest with this change to managedObjectHandler:

story.title = storyResponse.title + " (modified)"

您会看到丢失的故事被重新插入,这次它们的标题带有后缀 " (modified)";但以前喜欢的故事 不要修改(基本上,使用此设置,批量插入不会 re-insert objects)。

因此 isFavorite 属性 不会被覆盖,但 也不会更改任何应更改的属性 (因为他们收到了新标题,例如).

因此,如果您不希望更新 object,但希望插入全新的 object,则可以使用此方法。

但是,如果您希望 object 需要更新,这里有一些替代方案:

我认为实际上不可能通过批量插入请求进行部分更新。很难确定,因为我认为除了 WWDC 会议之外,没有任何这些记录。第一次看2019 session的时候很激动,因为主持人说:

Attributes that are optional or configured with default values can be omitted from the dictionary as well. In the case of updating an object with unique constraint, the existing values will not be changed.

我的意思是:

  • 您可以省略新 object 的值,您将获得默认值或 NULL。有道理。
  • 如果存在 object 而您省略了一个值,则该值不会更改。因此,您可以故意省略值以进行部分更新,即更新其他值,同时让 isFavorite 保持不变。

但是,在编写代码对此进行测试并查看 com.apple.CoreData.SQLDebug 的输出后,NSMergeByPropertyObjectTrumpMergePolicy 实际发生的情况似乎是:

  • 如果您省略了必需的值,则会出现验证错误。
  • 如果您省略一个可选值,它会将行更新为 NULL。对于 Swift 中的 Bool 属性,这将变为 false
  • 如果您省略具有默认值的值,它会将行更新为默认值。

这很遗憾,因为看起来部分更新 可以 通过让 ON CONFLICT 子句只为您实际需要的属性指定 DO UPDATE SET 来实现放。但是(从 macOS 11 开始)Core Data 似乎总是生成 SQL 来设置所有列。

总而言之,对于批量插入,NSMergeByPropertyObjectTrumpMergePolicy 实际上不会根据更改的内容 属性 进行合并(就像常规的 Core Data 保存一样)。相反,它要么插入一个新行(如果 object 不存在),要么覆盖所有列但保留 objectID(如果 object 存在)。

NSMergeByPropertyStoreTrumpMergePolicy 也不会被 属性 合并。它只是意味着如果存储的 object 已经存在,则单独保留它。

更新 (2021-06-24):我从 DTS 那里听说,Apple 认为上述当前 (iOS 14/macOS 11) 行为是一个错误,应该 让你批量插入而不改变省略的属性。雷达号是79747419.