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

Posted

技术标签:

【中文标题】如何避免更改 NSBatchInsertRequest 中的属性值?【英文标题】:How to avoid changing property values in an NSBatchInsertRequest? 【发布时间】:2021-04-04 06:21:04 【问题描述】:

我有一个简单的核心数据实体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 成为非可选布尔值没有 默认值(之前它被标记为可选,尽管我不确定它在这里有什么不同)并保持Use Scalar Type 框处于选中状态,我可以确认存储中的现有对象(根本不会)使用批量插入上下文的配置进行修改。

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)";但以前喜欢的故事 不要被修改(基本上,使用此设置,批量插入不会重新插入对象)。

所以isFavorite 属性不会被覆盖,但任何属性都不应更改(例如,因为它们收到了新标题)。

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

然而,如果您希望您的对象需要更新,这里有一些替代方法:

您可以选择在以这种方式运行批量插入后运行单独的更新操作,也可以是 NSBatchUpdateRequest, 或在批量插入之后,您可以在(可能是背景/子)上下文中的简单循环中更新某些属性,而无需批量操作,如果没有大量数据,这可能很好; 最后,您可以先将新数据批量插入临时存储,然后以某种方式手动将您选择的属性与新存储合并,然后删除临时存储。 一种更简单的方法:您可以在执行批量插入之前获取 all 想要保持不变的属性(将它们存储在由对象的唯一性约束值键入的字典中),然后在批量插入再次设置属性。 对于这种方法,您需要使用不同的合并策略,例如 NSMergeByPropertyObjectTrumpMergePolicy,以便将更新的对象重新插入到存储中(确保提前获取您不想丢失的所有属性批量插入) 随机想法:How to Save Data When Using One ManagedObjectContext and PersistentStoreCoordinator with Two Stores

【问题讨论】:

您对实体的唯一标识符有限制吗? 是的,奇怪的是设置context.persistentStoreCoordinator = container.persistentStoreCoordinator而不是父上下文是解决问题的(不令人满意的)解决方法 【参考方案1】:

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

可选的或配置有默认值的属性也可以从字典中省略。 在更新具有唯一约束的对象的情况下,现有值不会改变。

我的意思是:

您可以省略新对象的值,您将获得默认值或NULL。这是有道理的。 如果存在现有对象并且您省略了一个值,则该值不会更改。因此,您可以故意省略值以进行部分更新,即更新其他值,同时保留您的 isFavorite

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

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

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

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

NSMergeByPropertyStoreTrumpMergePolicy 也不会按属性合并。它只是意味着如果存储的对象已经存在,则不要管它。

更新 (2021-06-24):我从 DTS 获悉,Apple 认为上述当前 (ios 14/macOS 11) 行为是一个错误,并且它应该 允许您在没有更改省略的属性。雷达编号为 79747419。

【讨论】:

感谢您澄清和确认我的怀疑,这可能是一个错误,Michael。每次我刷新对 Core Data 的了解时,我都会对此感到困惑。 你是对的NSMergeByPropertyStoreTrumpMergePolicy:使用它只是防止重新插入具有匹配唯一值约束的对象,它实际上并没有更新现有对象。但是,除非我设置context.persistentStoreCoordinator = container.persistentStoreCoordinator,而不是context.parent = container.viewContext,否则现有对象也会被重新插入(并完全覆盖)。用我运行的实验更新了我的答案。 @AlexWalczak 我记得唯一性约束仅适用于 SQLite 存储。我不确定嵌套上下文是如何实现的,但我不希望约束在基于内存的父级甚至不适用于独立的NSInMemoryStoreType 时也能使用。 这个雷达有什么消息吗? iOS 15 Beta 2 / Xcode 13 Beta 2 似乎仍然存在此问题

以上是关于如何避免更改 NSBatchInsertRequest 中的属性值?的主要内容,如果未能解决你的问题,请参考以下文章

如何更改代码以避免运行时错误(AddressSanitizer)?

路由更改时如何避免构造函数调用?

画布高度由 EaselJS 更改。如何避免这种情况?

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

使用“select trim”时如何避免postgresql更改列名

如何避免“更改此条件,使其不总是评估为“假””