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

Posted

技术标签:

【中文标题】Restkit:执行我自己的 RKMapperOperation 时创建了重复的对象【英文标题】:Restkit: Duplicate objects getting created when performing my own RKMapperOperation 【发布时间】:2014-07-10 18:31:55 【问题描述】:

我有一个 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 代码,因为我很确定错误不存在 - 毕竟,Restkit 在执行映射器操作时已成功映射这些对象,即使存在冗余 JSON 对象/数据到地图。

我认为问题的另一个原因一定是我对托管对象上下文及其线程的实现不好,因为我正在 iPhone 5c 和 iPod touch 上进行测试,而问题不会发生在iPod touch,我相信它只有 1 个核心,但 iPhone 5c 确实有时会遇到这个问题,我相信它有多个核心。我应该强调,我不确定我在这一段中所做的陈述是否一定是真的,所以不要假设我知道我在谈论设备内核,这只是我认为我已经阅读过的东西之前。

【问题讨论】:

【参考方案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 的单元测试。您是否设置了标识属性以防止重复?可能不需要查找和设置目标对象,我认为如果未设置,RestKit 会尝试查找现有对象。还可以尝试在使用 [store newChildManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType trackingChanges: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);

【讨论】:

我认为你让我朝着正确的方向迈出了一步。我现在一直收到以下错误,在我很少看到它之前:“映射操作配置了 NSMainQueueConcurrencyType 并发类型的 managedObjectContext 并给定了一个 operationQueue 来执行后台工作。此配置将导致与 main 的死锁等待映射完成的队列和等待访问 MOC 的 operationQueue。您应该提供带有 NSPrivateQueueConcurrencyType 的 managedObjectContext。" 我相信它抱怨我给它的操作队列正在后台线程上调用主上下文。我应该给它“[NSOperationQueue mainQueue]”,还是应该为主上下文创建一个新的子上下文,或者别的什么? 由于删除了操作队列代码,我仍然得到重复。还有什么建议吗?非常感谢到目前为止的帮助。 我会考虑使用对象管理器操作队列,将其设置为一个并发操作,这样您就不能同时运行多个冲突负载。我还将使用标识 attrs 并且在开始操作(或者更确切地说,将操作添加到队列中)之前自己不明确地找到目标对象。我会使用主队列作为起点。目前,您总是在主线程和后台线程创建之间存在潜在的竞争,您需要一些方法来防止这种情况...... Panupan,感谢您的单元测试。今天晚些时候,当我回到我的开发计算机时,我将尝试实施一个解决方案,让您知道这是怎么回事。【参考方案2】:

这是我们采用的解决方案。确保在映射中设置了标识属性。在不设置destinationObject 的情况下使用RKMappingOperation,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];
];

【讨论】:

【参考方案3】:

请试一试,使用 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];
    ];

【讨论】:

以上是关于Restkit:执行我自己的 RKMapperOperation 时创建了重复的对象的主要内容,如果未能解决你的问题,请参考以下文章

Restkit 0.20:在自己的静态库中构建存档时找不到

CoreData + Restkit 当用户退出应用程序时要执行啥任务?

在 iOS 的 RestKit 中执行 postObject 时忽略响应

在 Restkit 执行映射操作之前删除 Core data 中的现有对象

在 Swift 中执行 Restkit addFetchRequestBlock 以进行孤儿删除

RestKit“无法在没有数据源的情况下执行关系映射”来自本地 JSON 文件