4
votes

I've got an iOS app that uses restkit to handle json responses to map things into core data. Anytime I perform a request through RKObjectManager's get/post/put/delete methods, it works great, and I never run into any issues.

The app I'm developing also supports socket updates, for which I'm using SocketIO to handle. SocketIO also is working flawlessly, and every event the server sends out I receive without fail, unless the app isn't running at that time.

The issue occurs when I try to take the json data from the socket event, and map it to core data. If the socket event comes in at the same time a a response comes back from a request I made through RKObjectManager, and they are both giving me the same object for the first time, they often both end up making 2 copies of the same ManagedObject in coredata, and I get the following warning in console:

Managed object Cache returned 2 objects for the identifier configured for the [modelObjectName] entity, expected 1.

Here is the method I've made containing the code for making the RKMapperOperation:

+(void)createOrUpdateObjectWithJSONDictionary:(NSDictionary*)jsonDictionary
{
    RKManagedObjectStore* managedObjectStore = [CMRAManager sharedInstance].objectManager.managedObjectStore;

    NSManagedObjectContext* context = managedObjectStore.mainQueueManagedObjectContext;

    [context performBlockAndWait:^{
        RKEntityMapping* modelEntityMapping = [self entityMappingInManagedObjectStore:managedObjectStore];

        NSDictionary* modelPropertyMappingsByDestinationKeyPath = modelEntityMapping.propertyMappingsByDestinationKeyPath;
        NSString* modelMappingObjectIdSourceKey = kRUClassOrNil([modelPropertyMappingsByDestinationKeyPath objectForKey:NSStringFromSelector(@selector(object_Id))], RKPropertyMapping).sourceKeyPath;
        NSString* modelObjectId = [jsonDictionary objectForKey:modelMappingObjectIdSourceKey];

        CMRARemoteObject* existingObject = [self searchForObjectOfCurrentClassWithId:modelObjectId];


        RKMapperOperation* mapperOperation = [[RKMapperOperation alloc]initWithRepresentation:jsonDictionary mappingsDictionary:@{ [NSNull null]: modelEntityMapping }];
        [mapperOperation setTargetObject:existingObject];

        RKManagedObjectMappingOperationDataSource* mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc]initWithManagedObjectContext:context cache:managedObjectStore.managedObjectCache];
        [mappingOperationDataSource setOperationQueue:[NSOperationQueue new]];
        [mappingOperationDataSource setParentOperation:mapperOperation];

        [mappingOperationDataSource.operationQueue setMaxConcurrentOperationCount:1];
        [mappingOperationDataSource.operationQueue setName:[NSString stringWithFormat:@"%@ with operation '%@'", NSStringFromSelector(_cmd), mapperOperation]];

        [mapperOperation setMappingOperationDataSource:mappingOperationDataSource];

        NSError* mapperOperationError = nil;
        BOOL mapperOperationSuccess = [mapperOperation execute:&mapperOperationError];
        NSAssert((mapperOperationError == nil) && (mapperOperationSuccess == TRUE), @"Execute mapperOperation error");
        if (mapperOperationError || (mapperOperationSuccess == FALSE))
        {
            NSLog(@"mapperOperationError: %@",mapperOperationError);
        }

        NSError* contextSaveError = nil;
        BOOL contextSaveSuccess = [context saveToPersistentStore:&contextSaveError];
        NSAssert((contextSaveError == nil) && (contextSaveSuccess == TRUE), @"Save context error");


    }];
}

In the above code, I first try and fetch the existing managed object if it currently exists to set it to the mapper request's target object. The method to find the object (searchForObjectOfCurrentClassWithId:) looks like the following:

+(instancetype)searchForObjectOfCurrentClassWithId:(NSString*)objectId
{
    NSManagedObjectContext* context = [CMRAManager sharedInstance].objectManager.managedObjectStore.mainQueueManagedObjectContext;

    __block CMRARemoteObject* fetchedObject = nil;

    [context performBlockAndWait:^{

        NSFetchRequest* fetchRequest = [self fetchRequestForCurrentClassObjectWithId:objectId];
        NSError* fetchError = nil;
        NSArray *entries = [context executeFetchRequest:fetchRequest error:&fetchError];

        if (fetchError)
        {
            NSLog(@"fetchError: %@",fetchError);
            return;
        }

        if (entries.count != 1)
        {
            return;
        }

        fetchedObject = kRUClassOrNil([entries objectAtIndex:0], CMRARemoteObject);
        if (fetchedObject == nil)
        {
            NSAssert(FALSE, @"Should be of this class");
            return;
        }

    }];

    return fetchedObject;
}

My best guess at the issue here is that it's probably due to the managed object contexts, and their threads. I don't have the best understanding of how they should necessarily be working, as I've been able to depend on Restkit's correct usage of it. I've done my best to copy how Restkit set up these mapping operations, but am assuming I've made an error somewhere in the above code.

I'm willing to post any other code if it would be helpful. I didn't post my RKEntityMapping code, because I'm pretty sure the error doesn't lie there - after all, Restkit has been successfully mapping these objects when it does the mapper operation itself, even when there's redundant JSON objects/data to map.

Another reason I think the issue must be my doing a bad implementation of the managed object contexts and their threads, is because I'm testing on an iPhone 5c, and an iPod touch, and the issue doesn't happen on the iPod touch, which I believe only has 1 core, but the iPhone 5c does sometimes encounter the issue, and I believe it has multiple cores. I should emphasize that I'm not sure of the statements I've made in this paragraph are necessarily true, so don't assume I know what I'm talking about with the device cores, it's just something I think I've read before.

3

3 Answers

1
votes

try changing:

RKManagedObjectMappingOperationDataSource* mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc]initWithManagedObjectContext:context cache:managedObjectStore.managedObjectCache];

to:

RKManagedObjectMappingOperationDataSource* mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc]initWithManagedObjectContext:context cache:[RKFetchRequestManagedObjectCache new]];

And this for good measure before saving the persistent context:

// Obtain permanent objectID
[[RKObjectManager sharedManager].managedObjectStore.mainQueueManagedObjectContext obtainPermanentIDsForObjects:[NSArray arrayWithObject:mapperOperation.targetObject] error:nil];

EDIT #1

Try removing these lines:

    [mappingOperationDataSource setOperationQueue:[NSOperationQueue new]];
    [mappingOperationDataSource setParentOperation:mapperOperation];

    [mappingOperationDataSource.operationQueue setMaxConcurrentOperationCount:1];
    [mappingOperationDataSource.operationQueue setName:[NSString stringWithFormat:@"%@ with operation '%@'", NSStringFromSelector(_cmd), mapperOperation]];

EDIT #2

Take a look at this unit test from RKManagedObjectMappingOperationDataSourceTest.m. Have you set identificationAttributes to prevent duplicates? It might not be necessary to find and set the targetObject, I thought RestKit tries to find an existing object if unset. Also try performing the object mapping on a private context created using [store newChildManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType tracksChanges:NO], after the context is saved, changes should be pushed to the main context.

- (void)testThatMappingObjectsWithTheSameIdentificationAttributesAcrossTwoContextsDoesNotCreateDuplicateObjects
{
    RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];
    RKInMemoryManagedObjectCache *inMemoryCache = [[RKInMemoryManagedObjectCache alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext];
    managedObjectStore.managedObjectCache = inMemoryCache;
    NSEntityDescription *humanEntity = [NSEntityDescription entityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext];
    RKEntityMapping *mapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore];
    mapping.identificationAttributes = @[ @"railsID" ];
    [mapping addAttributeMappingsFromArray:@[ @"name", @"railsID" ]];

    // Create two contexts with common parent
    NSManagedObjectContext *firstContext = [managedObjectStore newChildManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType tracksChanges:NO];
    NSManagedObjectContext *secondContext = [managedObjectStore newChildManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType tracksChanges:NO];

    // Map into the first context
    NSDictionary *objectRepresentation = @{ @"name": @"Blake", @"railsID": @(31337) };

    // Check that the cache contains a value for our identification attributes
    __block BOOL success;
    __block NSError *error;
    [firstContext performBlockAndWait:^{
        RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:firstContext
                                                                                                                                          cache:inMemoryCache];
        RKMapperOperation *mapperOperation = [[RKMapperOperation alloc] initWithRepresentation:objectRepresentation mappingsDictionary:@{ [NSNull null]: mapping }];
        mapperOperation.mappingOperationDataSource = dataSource;
        success = [mapperOperation execute:&error];
        expect(success).to.equal(YES);
        expect([mapperOperation.mappingResult count]).to.equal(1);

        [firstContext save:nil];
    }];

// Check that there is an entry in the cache
NSSet *objects = [inMemoryCache managedObjectsWithEntity:humanEntity attributeValues:@{ @"railsID": @(31337) } inManagedObjectContext:firstContext];
expect(objects).to.haveCountOf(1);

// Map into the second context
[secondContext performBlockAndWait:^{
    RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:secondContext
                                                                                                                                      cache:inMemoryCache];
    RKMapperOperation *mapperOperation = [[RKMapperOperation alloc] initWithRepresentation:objectRepresentation mappingsDictionary:@{ [NSNull null]: mapping }];
    mapperOperation.mappingOperationDataSource = dataSource;
    success = [mapperOperation execute:&error];
    expect(success).to.equal(YES);
    expect([mapperOperation.mappingResult count]).to.equal(1);

    [secondContext save:nil];
}];

// Now check the count
objects = [inMemoryCache managedObjectsWithEntity:humanEntity attributeValues:@{ @"railsID": @(31337) } inManagedObjectContext:secondContext];
expect(objects).to.haveCountOf(1);

// Now pull the count back from the parent context
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Human"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"railsID == 31337"];
NSArray *fetchedObjects = [managedObjectStore.persistentStoreManagedObjectContext executeFetchRequest:fetchRequest error:nil];
expect(fetchedObjects).to.haveCountOf(1);

}

0
votes

This is the solution we went with. Ensure identificationAttributes have been set in the mapping. Use RKMappingOperation without setting its destinationObject and RestKit will try to find an existing entity to map to by its identificationAttributes. We're also using RKFetchRequestManagedObjectCache as a precaution as we found the in-memory cache was unable to correct fetch the entities sometimes thus creating a duplicate entity..

NSManagedObjectContext *firstContext = [[NSManagedObjectContext alloc]        initWithConcurrencyType:NSPrivateQueueConcurrencyType];

firstContext.parentContext = [RKObjectManager sharedInstance].managedObjectStore.mainQueueManagedObjectContext;
firstContext.mergePolicy = NSOverwriteMergePolicy;

RKEntityMapping* modelEntityMapping = [self entityMappingInManagedObjectStore:[CMRAManager sharedInstance].objectManager.managedObjectStore];

RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:jsonDictionary destinationObject:nil mapping:modelEntityMapping];

// Restkit memory cache sometimes creates duplicates when mapping quickly across threads
RKManagedObjectMappingOperationDataSource *mappingDS = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:firstContext
                                                                                                                                         cache:[RKFetchRequestManagedObjectCache new]];

operation.dataSource = mappingDS;

NSError *mappingError;
[operation performMapping:&mappingError];
[operation waitUntilFinished];

if (mappingError || !operation.destinationObject) {
    return; // ERROR
}

[firstContext performBlockAndWait:^{
    [firstContext save:nil];
}];
0
votes

Please give this a try, use RKMappingOperation without setting the destination object, RestKit will try to find an existing object for you (if one exists) based on its identificationAttributes.

#pragma mark - Create or Update
+(void)createOrUpdateObjectWithJSONDictionary:(NSDictionary*)jsonDictionary
{
    RKEntityMapping* modelEntityMapping = [self entityMappingInManagedObjectStore:[CMRAManager sharedInstance].objectManager.managedObjectStore];

    // Map on the main MOC so that we receive the proper update notifications for anything
    // observing relationships and properties on this model
    RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:jsonDictionary
                                                                   destinationObject:nil
                                                                             mapping:modelEntityMapping];

    RKManagedObjectMappingOperationDataSource *mappingDS = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:[CMRAManager sharedInstance].objectManager.managedObjectStore.mainQueueManagedObjectContext
                                                                                                                                     cache:[RKFetchRequestManagedObjectCache new]];

    operation.dataSource = mappingDS;

    NSError *mappingError;
    [operation performMapping:&mappingError];

    if (mappingError || !operation.destinationObject) {
        return; // ERROR
    }

    // Obtain permanent objectID
    [[RKObjectManager sharedManager].managedObjectStore.mainQueueManagedObjectContext performBlockAndWait:^{
        [[RKObjectManager sharedManager].managedObjectStore.mainQueueManagedObjectContext obtainPermanentIDsForObjects:[NSArray arrayWithObject:operation.destinationObject] error:nil];
    }];
}