CoreData私有与公有存储无法同时去重问题求助
问题场景
按照苹果官方CoreData云同步文档实现了用户私有存储的实体去重功能,但应用同时包含需要去重的公有存储。当为私有和公有存储都启用NSPersistentStoreRemoteChangeNotificationPostOptionKey时,私有存储的去重功能失效;仅关闭公有存储的该选项时,私有存储去重才能正常工作。
原持久化配置代码
private static func makeStoreDescription(for database: Database) -> NSPersistentStoreDescription { let url: URL let scope: CKDatabase.Scope let configuration: String switch database { case .private: url = Self.privateStoreURL scope = .private configuration = "Private" case .public: url = Self.publicStoreURL scope = .public configuration = "Public" } let storeDescription = NSPersistentStoreDescription(url: url) storeDescription.cloudKitContainerOptions = .init(containerIdentifier: Config.cloudKitContainerIdentifier) storeDescription.cloudKitContainerOptions?.databaseScope = scope switch database { case .private: storeDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) storeDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) case .public: // Uncommented otherwise deduplication doesn't work // storeDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) // storeDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) break } storeDescription.configuration = configuration return storeDescription } private static func makeContainer(inMemory: Bool = false, useIcloud: Bool) -> NSPersistentCloudKitContainer { let privateStoreDescription = makeStoreDescription(for: .private) let publicStoreDescription = makeStoreDescription(for: .public) let container = NSPersistentCloudKitContainer(name: "Name", managedObjectModel: managedObjectModel) container.persistentStoreDescriptions = [privateStoreDescription, publicStoreDescription] // Don't save information for future use if running in memory... if inMemory { container.persistentStoreDescriptions.forEach { $0.url = URL(fileURLWithPath: "/dev/null") } } print("useIcloud:", useIcloud) if !useIcloud { container.persistentStoreDescriptions.forEach { $0.cloudKitContainerOptions = nil } } container.loadPersistentStores { storeDescription, error in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } print("Loaded store configuration:", storeDescription.configuration ?? "") } #if DEBUG // Use the container to initialize the development schema. // Only necessary whenever changes have been made to the schema. //try! container.initializeCloudKitSchema(options: []) #endif container.viewContext.automaticallyMergesChangesFromParent = true container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump container.viewContext.transactionAuthor = Self.transactionAuthor if !inMemory { do { try container.viewContext.setQueryGenerationFrom(.current) } catch { fatalError("Failed to pin viewContext to the current generation: \(error)") } } return container }
解决方案
问题根源
当两个存储同时启用远程通知和持久化历史跟踪时,CoreData的默认变更合并逻辑会混淆不同存储的变更事件,导致私有存储的去重逻辑被公有存储的变更干扰,最终失效。
实操步骤
1. 统一启用两个存储的核心配置
修改makeStoreDescription方法,为私有和公有存储都开启持久化历史跟踪与远程通知,确保两者的同步能力完整:
private static func makeStoreDescription(for database: Database) -> NSPersistentStoreDescription { let url: URL let scope: CKDatabase.Scope let configuration: String switch database { case .private: url = Self.privateStoreURL scope = .private configuration = "Private" case .public: url = Self.publicStoreURL scope = .public configuration = "Public" } let storeDescription = NSPersistentStoreDescription(url: url) storeDescription.cloudKitContainerOptions = .init(containerIdentifier: Config.cloudKitContainerIdentifier) storeDescription.cloudKitContainerOptions?.databaseScope = scope // 为两个存储统一启用必要选项 storeDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) storeDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) storeDescription.configuration = configuration return storeDescription }
2. 区分处理不同存储的远程变更
监听NSPersistentStoreRemoteChange通知时,通过通知的userInfo识别变更来源,为私有和公有存储分别执行独立的去重逻辑:
// 在makeContainer方法加载完存储后添加通知监听 private static func setupRemoteChangeHandling(container: NSPersistentCloudKitContainer) { NotificationCenter.default.addObserver(forName: .NSPersistentStoreRemoteChange, object: container, queue: .main) { notification in guard let userInfo = notification.userInfo, let storeURL = userInfo[NSPersistentStoreURLKey] as? URL else { return } switch storeURL { case Self.privateStoreURL: handlePrivateStoreDeduplication(container: container) case Self.publicStoreURL: handlePublicStoreDeduplication(container: container) default: break } } } // 私有存储去重逻辑(参考苹果文档实现,适配私有实体) private static func handlePrivateStoreDeduplication(container: NSPersistentCloudKitContainer) { let backgroundContext = container.newBackgroundContext() backgroundContext.performAndWait { // 此处实现私有实体的去重逻辑,例如查询重复记录并删除 // 示例: let fetchRequest: NSFetchRequest<NSFetchRequestResult> = YourPrivateEntity.fetchRequest() // 添加重复判断的谓词 fetchRequest.predicate = // 你的去重条件 // 执行查询并处理重复项 // ... } } // 公有存储去重逻辑 private static func handlePublicStoreDeduplication(container: NSPersistentCloudKitContainer) { let backgroundContext = container.newBackgroundContext() backgroundContext.performAndWait { // 此处实现公有实体的去重逻辑 // ... } }
然后在makeContainer方法的loadPersistentStores闭包后调用setupRemoteChangeHandling(container: container):
// ... container.loadPersistentStores { storeDescription, error in // ... 原有错误处理代码 } // 添加这行代码 setupRemoteChangeHandling(container: container) // ...
3. 优化视图上下文的合并策略
将视图上下文的合并策略调整为mergeByPropertyStoreTrump,优先保留存储中的数据,避免跨存储的实体冲突覆盖:
container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump
4. 确保实体与存储的绑定
在CoreData数据模型中,确认私有和公有实体分别绑定到对应的configuration(即代码中的"Private"和"Public"),避免实体在两个存储中混淆。
调试技巧
开启CoreData调试日志(添加启动参数-com.apple.CoreData.Logging.stderr 1),查看变更合并的详细流程,定位具体的冲突点。
内容的提问来源于stack exchange,提问作者Peter Warbo

