将记录 CKRecordID 保存到服务器时出错:尝试将记录从类型 'X' 更新为 'Y' 无效

Error saving record CKRecordID to server: invalid attempt to update record from type 'X' to 'Y'

当我使用 CKModifyRecordsOperation 将多个 table 的记录保存到私有云数据库的默认区域时,它总是 return 下面的错误除了 table 'X':

Error saving record to server: invalid attempt to update record from type 'X' to 'Y'

error.userInfo 详情:

{
CKErrorDescription = "Error saving record CKRecordID: 0x7fd7a3d4c0c0; 1:(_defaultZone:defaultOwner) to server: invalid attempt to update record from type 'X' to 'Y'";
ContainerID = "iCloud.com...";
NSDebugDescription = "CKInternalErrorDomain: 2006";
NSLocalizedDescription = "Error saving record to server: invalid attempt to update record from type 'X' to 'Y'";
NSUnderlyingError = "CKError 0x7fa0d250c4e0: \"Invalid Arguments\" (2006); server message = \"invalid attempt to update record from type 'X' to 'Y'\"; uuid = E2E...D1E; container ID = \"iCloud.com...\"";
RequestUUID = "E2E...D1E";
ServerErrorDescription = "invalid attempt to update record from type 'X' to 'Y'";
errorKey = ck1rosofi;
}

相关代码片段:

- (void)sync
{
  ...
  NSMutableArray * operations;
  for (NSString *tableName in @[@"X", @"Y"]) {
    CKModifyRecordsOperation * operation = [self _modifyRecordsOperationWithTableName:tableName];
    if (operation) {
      if (operations) [operations addObject:operation];
      else operations = [NSMutableArray arrayWithObject:operation];
    }
  }

  if (operations) {
    [operationQueue addOperations:operations waitUntilFinished:NO];
  }
}

- (CKModifyRecordsOperation *)_modifyRecordsOperationWithTableName:(NSString *)tableName
{
  ...

  NSMutableArray * recordsToSave = [NSMutableArray array];
  for (KYModel <KYModel_iCloudProtocol> *instance in unsyncedInstances) {
    CKRecordID * objectID = [[CKRecordID alloc] initWithRecordName:@(instance.id).stringValue];
    CKRecord * cloudRecord = [[CKRecord alloc] initWithRecordType:tableName recordID:objectID];
    ... setup record detail
    [recordsToSave addObject:record];
  }

  CKModifyRecordsOperation * operation = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:recordsToSave recordIDsToDelete:nil];
  operation.database = [[CKContainer defaultContainer] privateCloudDatabase];
  operation.savePolicy = CKRecordSaveAllKeys;
  operation.qualityOfService = NSQualityOfServiceUserInteractive;
  operation.atomic = NO;
  operation.perRecordProgressBlock = ...;
  operation.perRecordCompletionBlock = ^(CKRecord *record, NSError *error) {
    if (error) {
      // got error here
    }
    ...
  };
  operation.modifyRecordsCompletionBlock = ...;

  return operation;
}

经过一段时间的搜索和调试,我发现那些失败的记录在同一个目录中已经有相同的“记录名称”(它是 CKRecordID 的唯一名称,就像每个记录的唯一 ID)区域,尽管它们属于不同的 tables.


解决方案一:

使 CKRecordID 的“记录名称”在所有 Tables.

中唯一

例如为每条记录名称添加一个table名称前缀:[table_name]_[id].

我上面案例的问题是“记录名称”重复用于同一区域(默认区域)中不同 table 的记录。

并且使用CKModifyRecordsOperationCKRecordSaveAllKeys保存策略,当为tableY保存记录(id:1)时,它会找到[=的另一条记录(id:1) 77=] X at Cloud,并尝试对其进行修改,这会抛出错误 "invalid attempt to update record from type 'X' to 'Y'" 最后。


解决方案二:

为每个 Table 创建自定义区域(注意:自定义区域仅适用于私有云数据库)。

例如,不使用默认区域,而是为 table X 使用自定义区域“X_Zone”,为 table Y 使用自定义区域“Y_Zone”。然后我们可以继续使用@"id" 作为 CKRecordID 的"记录名称"。

这样,在将本地记录转换为CKRecord实例时,还需要提供zoneID

CKRecordZoneID *zoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Custom_Zone_Name_Here"
                                                         ownerName:CKOwnerDefaultName];
CKRecordID *objectID = [[CKRecordID alloc] initWithRecordName:@(instance.id).stringValue zoneID:zoneID];
...

当然,您需要先创建相关的自定义区域:

CKRecordZone *zone = [[CKRecordZone alloc] initWithZoneName:@"Custom_Zone_Name"];
[[[CKContainer defaultContainer] privateCloudDatabase] saveRecordZone:zone completionHandler:...];

或同时创建多个:

NSMutableArray *zones = [NSMutableArray array];
for (NSString *zoneName in @[...]) {
  CKRecordZone *zone = [[CKRecordZone alloc] initWithZoneName:zoneName];
  [zones addObject:zone];
}
CKModifyRecordZonesOperation * operation = [[CKModifyRecordZonesOperation alloc] initWithRecordZonesToSave:zones recordZoneIDsToDelete:nil];
...
[[[CKContainer defaultContainer] privateCloudDatabase] addOperation:operation];

我已经测试了两者并按预期工作。


建议:

如果您的记录存储在私有云数据库中,我认为解决方案二将是一个不错的选择,正如 DOC 所说:

... Use custom zones to arrange and encapsulate groups of related records in the private database. Custom zones support other capabilities too, such as the ability to write multiple records as a single atomic transaction.

Treat each custom zone as a single unit of data that is separate from every other zone in the database. Inside the zone, you add records as you would anywhere else. ...

处理每个 table 的记录会更方便,例如删除 table 的区域而不是查询和删除同一区域中 table 的记录。

但请注意,如果您在 table 中使用 CKReference,那么请不要为这些 table 使用自定义区域,因为

... the CKReference class does not support cross-zone linking, so each reference object must point to a record in the same zone as the current record.


顺便说一句,这是自答题的答案,希望对遇到同样问题的其他人有所帮助。如果我的回答有不对的地方请指出,也欢迎大家提出建议,提前谢谢 :)