CoreData存在批量更新操作时如何模拟SQLite事务行为
根因分析
你遇到的批量更新无法回滚的问题是CoreData的NSBatchUpdateRequest的固有特性导致的:
- 普通的增删改操作是在
NSManagedObjectContext的内存层完成的,调用save()之前所有变更都没有写入持久化存储,所以rollback()可以直接清空内存中的未提交变更,实现回滚。 - 而
NSBatchUpdateRequest属于直接操作底层持久化存储的请求,执行时会绕过上下文的内存缓存,直接把变更写入SQLite数据库,执行完成就已经生效了,和当前上下文是否save无关,所以上下文的rollback()自然管不到这部分变更。
解决方案
方案1:替换批量更新为上下文内普通更新(最稳妥,兼容所有场景)
如果你的待更新数据量不大(千级以内),直接把批量更新改成先fetch再逐个修改属性,所有变更都落在上下文内存中,就能和其他操作共享回滚能力:
把你renameLabels方法改成如下实现:
func renameLabels(_ backgroundContext: NSManagedObjectContext, from: String, to: String?, syncedTimestamp: Int64) throws { let fetchRequest: NSFetchRequest<NSPlainNote> = NSPlainNote.fetchRequest() fetchRequest.predicate = NSPredicate(format: "label == %@", from) let notes = try backgroundContext.fetch(fetchRequest) notes.forEach { note in note.label = to note.syncedTimestamp = syncedTimestamp } }
这个方案的优势是完全兼容你现有的事务模拟模板,不需要额外处理回滚逻辑,缺点是数据量过大会有内存开销。
方案2:延后批量更新执行时机+手动补偿回滚(适合大数据量场景)
如果数据量很大必须用批量更新,可以调整执行逻辑,确保只有所有校验都通过、确定要提交的时候再执行批量更新,如果后续save失败,手动执行反向批量更新恢复数据:
修改你的permanentDelete逻辑如下:
func permanentDelete(_ objectID: NSManagedObjectID) { let coreDataStack = CoreDataStack.INSTANCE let backgroundContext = coreDataStack.backgroundContext let currentTimeMillis = Date.currentTimeMillis backgroundContext.perform { // 标记是否已经执行了批量更新 var batchUpdateExecuted = false var originalName: String? = nil do { guard let nsTabInfo = try backgroundContext.existingObject(with: objectID) as? NSTabInfo else { return } guard let name = nsTabInfo.name else { return } originalName = name let uuid = nsTabInfo.uuid // 先执行所有内存层操作 backgroundContext.delete(nsTabInfo) _ = NSTabInfoTrash(context: backgroundContext, uuid: uuid, syncedTimestamp: currentTimeMillis) // 所有前置校验都通过,即将save前再执行批量更新 try renameLabels( backgroundContext, from: name, to: nil, syncedTimestamp: currentTimeMillis ) batchUpdateExecuted = true // 执行save try RepositoryUtils.saveContextIfPossible(backgroundContext) } catch { // 回滚内存变更 backgroundContext.rollback() error_log(error) // 如果批量更新已经执行,手动执行反向更新恢复数据 if batchUpdateExecuted, let originalName = originalName { do { let reverseBatchUpdate = NSBatchUpdateRequest(entityName: "NSPlainNote") reverseBatchUpdate.predicate = NSPredicate(format: "label == nil AND syncedTimestamp == %lld", currentTimeMillis) reverseBatchUpdate.propertiesToUpdate = [ "label": originalName, "syncedTimestamp": currentTimeMillis ] try backgroundContext.execute(reverseBatchUpdate) } catch let reverseError { error_log(reverseError) } } } } }
这个方案可以保留批量更新的高性能,只需要额外加反向恢复的逻辑即可。
内容的提问来源于stack exchange,提问作者Cheok Yan Cheng
相关产品推荐
相关产品推荐

