1

I have an entity (TestEntity) which contains a "Transformable" attribute which holds an object (MyObjectClass). On initial load, the transformable saves correctly; initialised as below:

TestEntity *test = (TestEntity *)[NSEntityDescription insertNewObjectForEntityForName:ENTITY[<Int>] inManagedObjectContext:temporaryContext];
test.transformableAttr = [[MyObjectClass alloc] initWithObject:obj];

However, when I fetch an object (I fetch as dictionary with NSDictionaryResultType) and update its "Transformable" attribute,

MyObjectClass *my_obj = ....
dict[@"transformableAttr"] = my_obj

it saves successfully but when I fetch it again I get nil for the "Transformable" attribute.

Now this only happens with "NSBatchUpdateRequest" because when I save using the MOC

TestEntity *test = ....
test.transformableAttr = updated_object

it saves successfully and I can access the updated attribute when fetched again.

Can anyone please explain? Does it mean that NSBatchUpdateRequest does not Transformable?

My NSBatchUpdateRequest code:

[context performBlock:^{
    NSError *requestError               = nil;
    NSBatchUpdateRequest *batchRequest  = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:entity];
    batchRequest.resultType             = NSUpdatedObjectIDsResultType;
    batchRequest.propertiesToUpdate     = properties;
    NSBatchUpdateResult *result         = nil;
    SET_IF_NOT_NIL(batchRequest.predicate, predicate)

    @try {
        result = (NSBatchUpdateResult *)[context executeRequest:batchRequest error:&requestError];
        if (requestError != nil){
            // @throw
        }

        if ([result.result respondsToSelector:@selector(count)]){
            __block NSInteger counter = [result.result count];

            if (counter > 0){
                [managedObjectContext performBlock:^{

                    for(NSManagedObjectID *objectID in result.result){
                        NSError *faultError = nil;
                        NSManagedObject *object = [managedObjectContext existingObjectWithID:objectID error:&faultError];

                        if (object && faultError == nil) {
                            [managedObjectContext refreshObject:object mergeChanges:YES];
                        }

                        counter--;
                        if (counter <= 0) {
                            // complete
                        }
                        else{
                            // Wait.
                        }
                    }
                }];
            }
            else{
                // No Changes
            }
        }
        else {
            // No Changes
        }
    }
    @catch (NSException *exception) {
        @throw;
    }
}];

1 Answers1

2

The documentation doesn't seem to call out this particular scenario, but I'm not surprised that it doesn't work. An NSBatchUpdateRequest is described as [emphasis mine]:

A request to Core Data to do a batch update of data in a persistent store without loading any data into memory.

Transformables work by converting to/from Data in memory. If your class conforms to NSCoding, the coding/decoding happens in memory, because SQLite doesn't know about NSCoding or your classes.

Your original assignment works because Core Data converts the value of transformableAttr to Data in memory and then saves the bytes of the Data to the persistent store. In the batch update, the objects aren't loaded into memory, so the transformation can't run, so the update doesn't work as you'd expect.

It's disappointing that Core Data doesn't make this clearer. Look in the Xcode console to see if it warns you about this. If it doesn't, please file a bug with Apple, because though I don't expect this to work, it's also not good for it to fail silently.

If you want to use batch updates, you'll have to convert your value in code before running the update. I'm not 100% certain how this will work but if your value conforms to NSCoding you'll start with

let transformedData: Data = NSKeyedArchiver.archivedData(withRootObject:transformableAttr)

What you do then is where I'm not sure. You might be able to use transformedData as the new value. Or you might have to access its bytes and use them somehow-- maybe using withUnsafeBytes(_:). You'll probably run into trouble because transformableAttr is not a Data, so it may get messy. It seems that batch updates aren't designed to work well with transformables.

Tom Harrington
  • 69,312
  • 10
  • 146
  • 170
  • Thanks for your answer, it did clarify my issue. I did use NSData which worked for both scenarios but over time the data got too large and impacted on performance. Unfortunately the log say nothing on NSBatchUpdateRequest and will file a bug with apple. – user3355301 Jan 24 '18 at 07:01
  • I was able to use [NSKeyedArchiver archivedDataWithRootObject:@"MyTestString"] as the value in the propertiesToEdit dictionary and it worked for my transformable attribute. I can't say how values other than NSString will work (didn't test). – mikemaat May 26 '20 at 18:20