iosobjective-ccloudkitckmodifyrecordsoperationckerror

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


While I'm using CKModifyRecordsOperation to save records for multiple tables to Private Cloud Database's Default Zone, it always return the error below except table 'X':

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

The error.userInfo detail:

{
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;
}

Related code snippets:

- (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;
}

Solution

  • After searching & debugging for a while, I realised that those failed records have same "Record Name" (it's an unique name for CKRecordID, which likes an unique ID for each record) already in the same zone, though they're belonging to different tables.


    Solution One:

    Making CKRecordID's "Record Name" Unique Among All Tables.

    For example, add a table name prefix for each record name: [table_name]_[id].

    The problem of my case above is that "Record Name" duplicated for records of different tables in same zone (Default Zone).

    And with CKModifyRecordsOperation's CKRecordSaveAllKeys saving policy, when save record (id:1) for table Y, it finds another record (id:1) of table X at Cloud, and tries to modify it, which throws the error "invalid attempt to update record from type 'X' to 'Y'" at last.


    Solution Two:

    Creating Custom Zones for Each Table (Note: Custom Zone is only available for Private Cloud Database).

    For example, instead of using the Default Zone, use custom zone "X_Zone" for table X, and "Y_Zone" for table Y. Then we can keep using @"id" as CKRecordID's "Record Name".

    In this way, you need to provide the zoneID as well when convert your local record to CKRecord instance:

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

    And of course, you need to create the related custom zone first:

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

    or create multiple at same time:

    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];
    

    I've tested both & works as expected.


    Suggestion:

    If your records are stored in Private Cloud Database, I think the Solution Two will be a good choice, as the DOC said:

    ... 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. ...

    It'll be much more convenient to handle records for each table, like deleting the zone for table instead of querying & deleting records for the table in same zone.

    BUT NOTE, if you use CKReference among tables, DO NOT use custom zones for those tables then, cause

    ... 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.


    Btw, this's the answer for the Self-Answered-Question, hope it helps if someone else get the same issue. Please point it out if there's something wrong in my answer, and welcome any suggestion as well, thx in advance. :)