如何在删除CloudKit记录时避免孤立记录?同步模式下的父子关系问题
CloudKit同步中删除操作与父子关系的问题解决思路
我之前在做类似的书籍-分类CloudKit同步功能时,也踩过不少删除和父子关系的坑,结合你的场景,咱们一步步拆解问题和解决方案:
一、先理清楚父子关系的基础配置(避免源头错误)
首先,确保你在创建书籍的category引用字段时,设置了正确的CKReferenceAction,这直接影响删除后的同步行为:
- 如果希望删除分类时,自动删除所有关联书籍:用
CKReferenceAction.deleteSelf - 如果希望删除分类后,书籍保留但引用字段置空:用
CKReferenceAction.nullify - 如果啥都不做(默认):
CKReferenceAction.none,这时候书籍的引用会变成无效,后续同步时容易触发错误
举个创建引用字段的代码示例:
let categoryReferenceField = CKRecord.FieldKey("category") let referenceConstraint = CKRecord.ReferenceConstraint( action: .deleteSelf, // 根据你的业务需求选合适的action referenceType: .toOne ) let recordTypeSchema = CKRecordType.Schema( recordType: "Book", fields: [ categoryReferenceField: CKRecord.Field.Schema( type: .reference, referenceConstraint: referenceConstraint ) ] ) // 上传schema到CloudKit(首次配置用)
二、本地变更追踪的正确姿势(针对删除操作)
你应该已经在本地维护了变更日志(比如Core Data的变更追踪,或者自定义的变更表),但针对删除操作,要注意:
- 不要直接删除本地记录:而是标记为「待删除」,并记录对应的
CKRecord.ID,直到同步到服务器并确认成功后,再彻底删除本地记录 - 对于父子关系的删除:比如删除分类时,要同时标记所有关联书籍为待删除吗?不用——如果你设置了
deleteSelf的引用action,服务器会自动处理级联删除,本地只需要标记分类为待删除即可;如果是nullify,本地要同步更新书籍的引用字段为null,再标记书籍为待更新
三、向服务器发送变更时的冲突处理
发送变更(包括删除)用CKModifyRecordsOperation时,要处理冲突和错误:
- 删除冲突:比如本地要删除一条记录,但服务器上这条记录已经被其他设备更新了。这时候在
modifyRecordsCompletionBlock里会收到CKError.serverRecordChanged错误,你需要:- 拉取服务器最新的记录版本
- 根据业务规则决定:是覆盖服务器版本继续删除,还是放弃删除并更新本地记录
- 父子关系的删除错误:比如你没设置
deleteSelf,却直接删除了分类,服务器会返回CKError.referenceViolation错误,这时候你需要先处理所有关联书籍(要么删除,要么更新引用),再重新删除分类
示例代码片段(处理删除冲突):
let operation = CKModifyRecordsOperation(recordsToSave: nil, recordIDsToDelete: [deletedRecordID]) operation.modifyRecordsCompletionBlock = { savedRecords, deletedRecordIDs, error in guard let error = error as? CKError else { return } if error.code == .serverRecordChanged { // 拉取服务器最新记录 let fetchOp = CKFetchRecordsOperation(recordIDs: [deletedRecordID]) fetchOp.fetchRecordsCompletionBlock = { records, fetchError in guard let serverRecord = records?.first?.value else { return } // 业务决策:比如弹出提示让用户选择,或者自动放弃删除并更新本地 if shouldForceDelete { // 用服务器最新的recordID重新发起删除(带上etag) let forceDeleteOp = CKModifyRecordsOperation(recordsToSave: nil, recordIDsToDelete: [serverRecord.recordID]) forceDeleteOp.savePolicy = .ifServerRecordUnchanged // 添加到操作队列执行 } else { // 更新本地记录为服务器版本 updateLocalRecord(from: serverRecord) } } CKContainer.default().privateCloudDatabase.add(fetchOp) } } CKContainer.default().privateCloudDatabase.add(operation)
四、用CKFetchRecordChangesOperation处理服务器变更
当拉取服务器变更时,要同时处理changedRecords和deletedRecordIDs,并且注意父子关系的联动:
- 处理服务器删除的记录:
- 如果是分类被删除:
- 如果你设置了
deleteSelf,服务器已经自动删除了关联书籍,所以本地要删除对应的分类和所有关联书籍 - 如果是
nullify,本地要把关联书籍的category字段设为null,然后保存本地变更
- 如果你设置了
- 如果是书籍被删除:本地直接删除对应的书籍记录即可
- 如果是分类被删除:
- 处理服务器更新的记录:比如服务器上书籍的分类引用被修改了,本地要同步更新这个引用,避免父子关系不一致
示例代码片段(处理服务器删除的分类):
let operation = CKFetchRecordChangesOperation( recordZoneID: CKRecordZone.default().zoneID, previousServerChangeToken: self.serverChangeToken ) operation.recordChangedBlock = { record in // 更新本地记录 updateLocalRecord(from: record) } operation.recordDeletedBlock = { recordID in // 判断删除的是分类还是书籍 fetchLocalRecordType(for: recordID) { recordType in switch recordType { case "Category": // 获取本地所有关联该分类的书籍 let relatedBooks = fetchLocalBooks(forCategoryID: recordID.recordName) if referenceActionIsDeleteSelf { // 删除本地分类和所有书籍 deleteLocalRecords([recordID] + relatedBooks.map { $0.recordID }) } else { // 更新书籍的category字段为null relatedBooks.forEach { book in book.category = nil markBookAsNeedingUpdate(book) } // 删除本地分类 deleteLocalRecord(withID: recordID) } case "Book": deleteLocalRecord(withID: recordID) default: break } } } operation.fetchRecordChangesCompletionBlock = { newToken, moreComing, error in if let error = error { // 处理错误,比如重试 } else { // 保存新的服务器变更令牌 self.serverChangeToken = newToken if moreComing { // 还有更多变更,继续发起操作 fetchNextBatchOfChanges() } } } CKContainer.default().privateCloudDatabase.add(operation)
五、最后的注意事项
- 原子性操作:如果需要同时删除分类和书籍,尽量用
CKModifyRecordsOperation一次性提交所有删除请求,避免部分成功部分失败的情况 - 本地数据一致性:同步前后都要保证本地的书籍和分类关系是正确的,比如每次同步后检查有没有书籍的分类引用指向不存在的分类,及时修复
- 测试边缘场景:比如离线时删除分类,然后上线同步;多设备同时删除同一分类/书籍;服务器自动级联删除后本地同步的情况
内容的提问来源于stack exchange,提问作者Bill
相关产品推荐
相关产品推荐

