Restkit:执行我自己的RKMapperOperation时创建的重复对象

时间:2014-07-10 18:31:56

标签: ios core-data restkit

我有一个iOS应用程序,它使用restkit来处理json响应,将事物映射到核心数据中。无论何时我通过RKObjectManager的get / post / put / delete方法执行请求,它都很有用,我从来没有遇到过任何问题。

我正在开发的应用程序也支持套接字更新,我正在使用SocketIO来处理它。 SocketIO也正在完美运行,服务器发出的每个事件我都会收到,除非应用程序当时没有运行。

当我尝试从套接字事件中获取json数据并将其映射到核心数据时,会出现此问题。如果套接字事件同时进入,响应从我通过RKObjectManager发出的请求返回,并且它们都是第一次给我相同的对象,它们通常最终在coredata中制作相同ManagedObject的2个副本,我在控制台中收到以下警告:

托管对象缓存为[modelObjectName]实体配置的标识符返回了2个对象,预期为1。

这是我制作的包含制作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");


    }];
}

在上面的代码中,我首先尝试获取现有的托管对象(如果它当前存在)以将其设置为映射器请求的目标对象。查找对象的方法(searchForObjectOfCurrentClassWithId :)如下所示:

+(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代码,因为我很确定错误并不存在 - 毕竟,当它执行mapper操作时,Restkit已成功映射这些对象,即使有多余的JSON对象/数据也是如此地图。

我认为问题必须是我对托管对象上下文及其线程执行不良的另一个原因是因为我在iPhone 5c和iPod touch上进行测试,问题不会发生在iPod touch,我相信只有1个核心,但iPhone 5c确实有时遇到这个问题,我相信它有多个核心。我应该强调一点,我不确定我在本段中所做的陈述是否一定都是正确的,所以不要以为我知道我在谈论器件内核,这只是我认为我读过的内容之前。

3 个答案:

答案 0 :(得分:1)

尝试改变:

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

从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 :(得分:0)

请试一试,在不设置目标对象的情况下使用RKMappingOperation,RestKit将尝试根据其identifyAttributes为您查找现有对象(如果存在)。

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

答案 2 :(得分:0)

这是我们采用的解决方案。确保已在映射中设置了identifyAttributes。使用RKMappingOperation而不设置其destinationObject,RestKit将尝试通过其identificationAttributes查找要映射到的现有实体。我们也使用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];
}];