我们的每个数据行都包含一个唯一的uuid列.

以前,在采用CloudKit之前,uuid列有一个唯一的约束.这使我们能够防止数据重复.

现在,我们开始将CloudKit集成到现有的CoreData中.这样的唯一约束将被删除.以下用户流将导致数据重复.

使用CloudKit时导致数据重复的步骤

  1. 首次启动应用程序.
  2. 由于存在空数据,因此生成具有预定义uuid的预定义数据.
  3. 预定义数据与iCloud同步.
  4. 应用程序已卸载.
  5. 应用程序已重新安装.
  6. 首次启动应用程序.
  7. 由于存在空数据,因此生成具有预定义uuid的预定义数据.
  8. 步骤3中以前的旧预定义数据与设备同步.
  9. 我们现在有2个预定义数据,具有相同的uuid!:(

我想知道,我们有没有办法防止这种重复?

在步骤8中,我们希望在写入CoreData之前能够执行这样的逻辑

判断CoreData中是否存在此类uuid.如果没有,则写入CoreData.

我曾经try 将上述逻辑插入https://developer.apple.com/documentation/coredata/nsmanagedobject/1506209-willsave.为了防止保存,我使用self.managedObjectContext?.rollback().但它只是崩溃了.

Do you have any idea, what are some reliable mechanism I can use, to prevent data duplication in CoreData CloudKit?


其他信息:

在采用CloudKit之前

我们使用以下CoreData堆栈

class CoreDataStack {
    static let INSTANCE = CoreDataStack()
    
    private init() {
    }
    
    private(set) lazy var persistentContainer: NSPersistentContainer = {
        precondition(Thread.isMainThread)
        
        let container = NSPersistentContainer(name: "xxx", managedObjectModel: NSManagedObjectModel.wenote)
        
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // This is a serious fatal error. We will just simply terminate the app, rather than using error_log.
                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
        
        // TODO: Not sure these are required...
        //
        //container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        //container.viewContext.undoManager = nil
        //container.viewContext.shouldDeleteInaccessibleFaults = true
        
        return container
    }()

我们的CoreData数据模式

  1. 唯一的约束.
  2. Deny关系的删除规则.
  3. 没有非null字段的默认值.

采用CloudKit后

class CoreDataStack {
    static let INSTANCE = CoreDataStack()
    
    private init() {
    }
    
    private(set) lazy var persistentContainer: NSPersistentContainer = {
        precondition(Thread.isMainThread)
        
        let container = NSPersistentCloudKitContainer(name: "xxx", managedObjectModel: NSManagedObjectModel.wenote)
        
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // This is a serious fatal error. We will just simply terminate the app, rather than using error_log.
                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
        
        // TODO: Not sure these are required...
        //
        //container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        //container.viewContext.undoManager = nil
        //container.viewContext.shouldDeleteInaccessibleFaults = true
        
        return container
    }()

我们将CoreData数据模式更改为

  1. Not having唯一约束.
  2. Nullify关系的删除规则.
  3. 具有非空字段的默认值.

根据https://developer.apple.com/forums/thread/699634?login=true名开发人员技术支持工程师的反馈,hen提到我们可以

  1. 通过使用存储持久历史记录检测相关更改
  2. 删除重复数据

但是,由于所提供的github链接已断开,因此它应该如何实现还不完全清楚.

推荐答案

一旦我们与CloudKit集成,就有no unique constraint个功能.

此限制的解决方法是

一旦CloudKit插入后检测到重复,我们将

此解决方案的挑战部分是,how can we be notified when there is insertion performed by CloudKit?

下面逐步介绍如何在CloudKit执行插入时得到通知.

  1. 在CoreData中启用NSPersistentHistoryTrackingKey feature.
  2. 在CoreData中启用NSPersistentStoreRemoteChangeNotificationPostOptionKey feature.
  3. 设置viewContext.transactionAuthor = "app".这是一个重要的步骤,因此当我们查询事务历史时,我们知道哪个DB事务是由我们的应用程序启动的,哪个DB事务是由CloudKit启动的.
  4. 每当通过NSPersistentStoreRemoteChangeNotificationPostOptionKey功能自动通知我们时,我们将开始查询交易历史记录.查询将根据transaction authorlast query token进行过滤.有关更多详细信息,请参阅代码示例.
  5. 一旦我们检测到事务为insert,并且它在相关entity上运行,我们将开始基于相关entity执行重复数据删除

代码示例

import CoreData

class CoreDataStack: CoreDataStackable {
    let appTransactionAuthorName = "app"
    
    /**
     The file URL for persisting the persistent history token.
    */
    private lazy var tokenFile: URL = {
        return UserDataDirectory.token.url.appendingPathComponent("token.data", isDirectory: false)
    }()
    
    /**
     Track the last history token processed for a store, and write its value to file.
     
     The historyQueue reads the token when executing operations, and updates it after processing is complete.
     */
    private var lastHistoryToken: NSPersistentHistoryToken? = nil {
        didSet {
            guard let token = lastHistoryToken,
                let data = try? NSKeyedArchiver.archivedData( withRootObject: token, requiringSecureCoding: true) else { return }
            
            if !UserDataDirectory.token.url.createCompleteDirectoryHierarchyIfDoesNotExist() {
                return
            }
            
            do {
                try data.write(to: tokenFile)
            } catch {
                error_log(error)
            }
        }
    }
    
    /**
     An operation queue for handling history processing tasks: watching changes, deduplicating tags, and triggering UI updates if needed.
     */
    private lazy var historyQueue: OperationQueue = {
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 1
        return queue
    }()
    
    var viewContext: NSManagedObjectContext {
        persistentContainer.viewContext
    }
    
    static let INSTANCE = CoreDataStack()
    
    private init() {
        // Load the last token from the token file.
        if let tokenData = try? Data(contentsOf: tokenFile) {
            do {
                lastHistoryToken = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: tokenData)
            } catch {
                error_log(error)
            }
        }
    }
    
    deinit {
        deinitStoreRemoteChangeNotification()
    }
    
    private(set) lazy var persistentContainer: NSPersistentContainer = {
        precondition(Thread.isMainThread)
        
        let container = NSPersistentCloudKitContainer(name: "xxx", managedObjectModel: NSManagedObjectModel.xxx)
        
        // turn on persistent history tracking
        let description = container.persistentStoreDescriptions.first
        description?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
        description?.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
        
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // This is a serious fatal error. We will just simply terminate the app, rather than using error_log.
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        
        // Provide transaction author name, so that we can know whether this DB transaction is performed by our app
        // locally, or performed by CloudKit during background sync.
        container.viewContext.transactionAuthor = appTransactionAuthorName
        
        // So that when backgroundContext write to persistent store, container.viewContext will retrieve update from
        // persistent store.
        container.viewContext.automaticallyMergesChangesFromParent = true
        
        // TODO: Not sure these are required...
        //
        //container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        //container.viewContext.undoManager = nil
        //container.viewContext.shouldDeleteInaccessibleFaults = true
        
        // Observe Core Data remote change notifications.
        initStoreRemoteChangeNotification(container)
        
        return container
    }()
    
    private(set) lazy var backgroundContext: NSManagedObjectContext = {
        precondition(Thread.isMainThread)
        
        let backgroundContext = persistentContainer.newBackgroundContext()

        // Provide transaction author name, so that we can know whether this DB transaction is performed by our app
        // locally, or performed by CloudKit during background sync.
        backgroundContext.transactionAuthor = appTransactionAuthorName
        
        // Similar behavior as Android's Room OnConflictStrategy.REPLACE
        // Old data will be overwritten by new data if index conflicts happen.
        backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        
        // TODO: Not sure these are required...
        //backgroundContext.undoManager = nil
        
        return backgroundContext
    }()
    
    private func initStoreRemoteChangeNotification(_ container: NSPersistentContainer) {
        // Observe Core Data remote change notifications.
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(storeRemoteChange(_:)),
            name: .NSPersistentStoreRemoteChange,
            object: container.persistentStoreCoordinator
        )
    }
    
    private func deinitStoreRemoteChangeNotification() {
        NotificationCenter.default.removeObserver(self)
    }
    
    @objc func storeRemoteChange(_ notification: Notification) {
        // Process persistent history to merge changes from other coordinators.
        historyQueue.addOperation {
            self.processPersistentHistory()
        }
    }
    
    /**
     Process persistent history, posting any relevant transactions to the current view.
     */
    private func processPersistentHistory() {
        backgroundContext.performAndWait {
            
            // Fetch history received from outside the app since the last token
            let historyFetchRequest = NSPersistentHistoryTransaction.fetchRequest!
            historyFetchRequest.predicate = NSPredicate(format: "author != %@", appTransactionAuthorName)
            let request = NSPersistentHistoryChangeRequest.fetchHistory(after: lastHistoryToken)
            request.fetchRequest = historyFetchRequest

            let result = (try? backgroundContext.execute(request)) as? NSPersistentHistoryResult
            guard let transactions = result?.result as? [NSPersistentHistoryTransaction] else { return }

            if transactions.isEmpty {
                return
            }
            
            for transaction in transactions {
                if let changes = transaction.changes {
                    for change in changes {
                        let entity = change.changedObjectID.entity.name
                        let changeType = change.changeType
                        let objectID = change.changedObjectID
                        
                        if entity == "NSTabInfo" && changeType == .insert {
                            deduplicateNSTabInfo(objectID)
                        }
                    }
                }
            }
            
            // Update the history token using the last transaction.
            lastHistoryToken = transactions.last!.token
        }
    }
    
    private func deduplicateNSTabInfo(_ objectID: NSManagedObjectID) {
        do {
            guard let nsTabInfo = try backgroundContext.existingObject(with: objectID) as? NSTabInfo else { return }
            
            let uuid = nsTabInfo.uuid
            
            guard let nsTabInfos = NSTabInfoRepository.INSTANCE.getNSTabInfosInBackground(uuid) else { return }
            
            if nsTabInfos.isEmpty {
                return
            }
            
            var bestNSTabInfo: NSTabInfo? = nil
            
            for nsTabInfo in nsTabInfos {
                if let _bestNSTabInfo = bestNSTabInfo {
                    if nsTabInfo.syncedTimestamp > _bestNSTabInfo.syncedTimestamp {
                        bestNSTabInfo = nsTabInfo
                    }
                } else {
                    bestNSTabInfo = nsTabInfo
                }
            }
            
            for nsTabInfo in nsTabInfos {
                if nsTabInfo === bestNSTabInfo {
                    continue
                }
                
                // Remove old duplicated data!
                backgroundContext.delete(nsTabInfo)
            }
            
            RepositoryUtils.saveContextIfPossible(backgroundContext)
        } catch {
            error_log(error)
        }
    }
}

参考

  1. https://developer.apple.com/documentation/coredata/synchronizing_a_local_store_to_the_cloud-在示例代码中,文件CoreDataStack.swift演示了一个类似的示例,说明了如何在云同步后删除重复数据.
  2. https://developer.apple.com/documentation/coredata/consuming_relevant_store_changes-交易历史记录信息.
  3. What's the best approach to prefill Core Data store when using NSPersistentCloudKitContainer? - A similar question

Ios相关问答推荐

StackView UIKit间距似乎非常小的编程式编程

DriverKit驱动程序中可能存在IOBufferMemoyDescriptor泄漏

如何从Windows PC在iPhone上安装Flutter 应用程序

如何使用参与者允许并行读取,但阻止对SWIFT中资源的并发读取和写入?

由于已存在同名项目,因此无法将Mapbox.xcframework-ios.sign复制到Signature中

iOS应用程序冻结在UICollectionView代码,但我没有';没有任何UICollectionViews

从包含 5 个以上项目的自定义选项卡栏的导航视图中删除更多按钮

Swift Combine和iOS版本限制?

为什么 Firebase Messaging 配置对于 IOS native 和 Flutter iOS 不同?

如何将 -fobjc-arc-exceptions 标志正确添加到 XCode 中?

叠加视图不在堆栈中维护自身 - SwiftUI

本月使用 iOS 的天数?

所有小部件的Flutter 填充?

Xcode:此时无法安装此应用程序.

如何将自己项目中的类导入 Playground

在 iOS 上存储身份验证令牌 - NSUserDefaults 与 keys 串?

iOS:设备旋转后如何运行函数(Swift)

文本未包含在 swift UI 中

闭包不能隐式捕获变异的 self 参数

找不到开发者磁盘映像