如何在数据库事务中多次调用 NSBatchUpdateRequest,以便更新所有行或更新 none?

How can I make multiple calls of NSBatchUpdateRequest within DB transaction so that either all rows is updated or none is updated?

有没有办法在数据库事务中执行多个 NSBatchUpdateRequest 调用,以便更新所有数据库行或更新 none(抛出异常时)?

下面的代码说明了这个问题。


func debug() {
    let coreDataStack = CoreDataStack.INSTANCE
    let backgroundContext = coreDataStack.backgroundContext
    
    backgroundContext.perform {
        let fetchRequest = NSTabInfo.fetchSortedRequest()
        
        do {
            var objectIDs: [NSManagedObjectID] = []
            
            let nsTabInfos = try fetchRequest.execute()
            
            //
            // QUESTION: We are updating multiple rows of data directly in a persistent store.
            // How can we ensure either all rows is updated, or none row is updated is exception
            // happens in between?
            //
            for nsTabInfo in nsTabInfos {
                let batchUpdateRequest = NSBatchUpdateRequest(entityName: "NSTabInfo")
                batchUpdateRequest.predicate = NSPredicate(format: "self == %@", nsTabInfo.objectID)
                batchUpdateRequest.propertiesToUpdate = ["name": nsTabInfo.name! + "XXX"]
                batchUpdateRequest.resultType = .updatedObjectIDsResultType
                
                let batchUpdateResult = try backgroundContext.execute(batchUpdateRequest) as? NSBatchUpdateResult
                
                guard let batchUpdateResultX = batchUpdateResult else { return }
                guard let managedObjectIDs = batchUpdateResultX.result else { return }

                if let nsManagedObjectIDs = managedObjectIDs as? [NSManagedObjectID] {
                    objectIDs.append(contentsOf: nsManagedObjectIDs)
                }
                
                //
                // Simulate some exception
                // We notice the first row is updated & rest of the rows are unchanged.
                // This leaves our data in inconsistent state.
                //
                throw "Custom error!!!"
            }
            
            if !objectIDs.isEmpty {
                let changes = [NSUpdatedObjectsKey : objectIDs]
                coreDataStack.mergeChanges(changes)
            }
            
        } catch {
            backgroundContext.rollback()
            
            error_log(error)
        }
    }
}

class CoreDataStack {
    static let INSTANCE = CoreDataStack()
    
    private init() {
    }
    
    private(set) lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "wenote")
        
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        
        // So that when backgroundContext write to persistent store, container.viewContext will retrieve update from
        // persistent store.
        container.viewContext.automaticallyMergesChangesFromParent = true
        
        return container
    }()
    
    private(set) lazy var backgroundContext: NSManagedObjectContext = {
        let backgroundContext = persistentContainer.newBackgroundContext()

        backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        
        return backgroundContext
    }()
    

    func mergeChanges(_ changes: [AnyHashable : Any]) {
        
        NSManagedObjectContext.mergeChanges(
            fromRemoteContextSave: changes,
            into: [persistentContainer.viewContext, backgroundContext]
        )
    }
}

下面我们写一个demo代码来说明一下

  1. 在循环中多次执行 NSBatchUpdateRequest
  2. 中间出现异常
  3. 我们希望 none 更新持久存储中的行。但是,在抛出异常之前已经更新了一行。

请问我可以使用什么技术,类似于SQLite事务功能,以便更新所有行,或者在发生异常时更新该行的none?

CoreData.framework 不向用户开放 SQLite 级控制,它为您提供 NSManagedObjectContext.

它如何以类似的方式工作?

  1. 您可以根据需要拉取尽可能多的对象并对其进行更改。
  2. 完成更改后,您将执行 context.save()
  3. 这样一来,您可以一次性保存所有更改。

在所有情况下,将所有对象都拉入内存可能是不可能的,也不是一个好主意,因此您需要围绕如何将所有这些更改发送到磁盘来实施自己的解决方案。

来自 NSBatchUpdateRequest 文档 -

A request to Core Data to do a batch update of data in a persistent store without loading any data into memory.

当您执行此操作时,您正在对无法回滚的存储区进行更改。对于大型数据集,您可以执行以下操作 -

  1. 假设您必须对 10 万条记录执行一系列更新(5 个不同的步骤)作为一个操作。
  2. 在后台线程中启动,一次以 1k 为单位提取内存中的对象。
  3. 您可以轻松地在内存中加载 1k 个对象,改变它们 - 一个一个地检查所有 changes/steps 并将这些更改保存到这批。如果成功,您将继续下一批。
  4. 如果一个批次的中间步骤失败,您可以根据您的实施使用 NSManagedObjectContext.rollback() or NSManagedObjectContext.reset()

Here's a popular SO post 关于两者之间的区别,以防官方文档不够清晰。