Restkit: creating duplicate objects when running my own RKMapperOperation

I have an iOS app that uses restkit to handle json responses to map things to master data. Every time I execute a query using the RKObjectManager get / post / put / delete methods, it works fine and I never run into any problems.

The application I am developing also supports socket updates, for which I use SocketIO for processing. SocketIO also works flawlessly, and every event that the server sends, I receive without fail if the application does not work at this time.

The problem occurs when I try to extract json data from a socket event and match it with the main data. If the socket event arrives at the same time, the response is returned from the request I made through RKObjectManager, and both of them give me the same object for the first time, they often both make 2 copies of the same ManagedObject in coredata, and I get the following warning in console:

Managed Object The cache returned 2 objects for the identifier configured for the object [modelObjectName], expected 1.

Here is my method containing the code to create 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 will first try and fetch the existing managed entity, if it currently exists, to set it to the target of the mapping request. The method for finding an object (searchForObjectOfCurrentClassWithId :) is as follows:

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

, , , . , , Restkit. , , Restkit , , - .

, . RKEntityMapping, , . , Restkit , mapper, / JSON .

, , , , iPhone 5c iPod touch, iPod touch, , , 1 , iPhone 5c , , . , , , , , , , , , , .

+4
3

, . , . RKMappingOperation destinationObject, RestKit . RKFetchRequestManagedObjectCache , , , .

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

:

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

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

, :

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

№ 1

:

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

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

№ 2

unit test RKManagedObjectMappingOperationDataSourceTest.m. ? , targetObject, , RestKit , . , [store newChildManagedObjectContextWithConcurrencyType: NSPrivateQueueConcurrencyType tracksChanges: NO] , .

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

}

+1

, RKMappingOperation , RestKit ( ) .

#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];
    }];
}
0

Source: https://habr.com/ru/post/1547967/


All Articles