多文本核心数据:合并到未保存的上下文

时间:2015-02-14 20:45:32

标签: ios core-data

我试图实现这个核心数据堆栈:

PSC <--+-- MainMOC
       |
       +-- BackgroundPrivateMOC

有些事情我实际上并不了解。也许我们在Persisten Store中有一个对象,我们从主MOC中获取它以进行一些更改(用户手动更改它)。同时我的BG MOC正在使用相同的一个对象进行一些更改并将更改保存到PS。保存完成后,我们必须将BG MOC合并到MAIN MOC(这是一种常见做法)。合并后我的期望是MAIN MOC包含BG MOC的变化(因为更改比MAIN更晚了)。但这实际上并没有发生。完成合并后我所拥有的只是我MAIN MOC中的一个脏refreshedObjects = 1如果我通过MAIN MOC再次获取该对象,我看不到通过BG MOC所做的任何更改。

  • 我应该如何正确地将BG更改传播到MAIN MOC 在进行BG更改之前,MAIN MOC没有保存?
  • 如何处理 合并完成后我的MAIN MOC非零refreshedObjects的情况,以及 如何在MAIN MOC中推送这些对象以使它们可用 fetch和with?

我相信我的示例代码可以帮助您更清楚地理解我的问题。您可以下载项目(https://www.dropbox.com/s/1qr50zto5j4hj40/ThreadedCoreData.zip?dl=0)并运行我准备的XCTest。

以下是失败的测试代码:

@implementation ThrdCoreData_Tests

- (void)setUp
{
    [super setUp];

/**
 OUR SIMPLE STACK:

    PSC <--+-- MainMOC
           |
           +-- BackgroundPrivateMOC

 */
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];

    // main context (Main queue)
    _mainMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    [_mainMOC setPersistentStoreCoordinator:coordinator];
    [_mainMOC setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];

    // background context (Private Queue)
    _bgMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    _bgMOC.persistentStoreCoordinator = self.persistentStoreCoordinator;
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(mergeBGChangesToMain:)
                                                 name:NSManagedObjectContextDidSaveNotification
                                               object:_bgMOC];

    u_int32_t value = arc4random_uniform(3000000000); // simply generate new random values for the test
    _mainMOCVlaue = [NSString stringWithFormat:@"%u" , value];
    _expectedBGValue = [NSString stringWithFormat:@"%u" , value/2];

    Earthquake * mainEq = [Earthquake MR_findFirstInContext:self.mainMOC];
    if (!mainEq){  // At the very first time the test is running, create one single test oject.
        Earthquake * mainEq = [Earthquake MR_createEntityInContext:self.mainMOC];
        mainEq.location = nil; // initial value will be nil
        [self.mainMOC MR_saveOnlySelfAndWait];
    }
}

- (void)testThatBGMOCSuccessfullyMergesWithMain
{
    _expectation = [self expectationWithDescription:@"test finished"];

    // lets change our single object in main MOC. I expect that the value will be later overwritten by `_expectedBGValue`
    Earthquake * mainEq = [Earthquake MR_findFirstInContext:self.mainMOC];
    NSLog(@"\nCurrently stored value:\n%@\nNew main value:\n%@", mainEq.location, _mainMOCVlaue);
    mainEq.location = _mainMOCVlaue; // the test will succeed if this line commented

    // now change that object in BG MOC by setting `_expectedBGValue`
    [_bgMOC performBlockAndWait:^{
        Earthquake * bgEq = [Earthquake MR_findFirstInContext:_bgMOC];
        bgEq.location = _expectedBGValue;
        NSLog(@"\nNew expected value set:\n%@", _expectedBGValue);
        [_bgMOC MR_saveToPersistentStoreAndWait]; // this will trigger the `mergeBGChangesToMain` method
    }];

    [self waitForExpectationsWithTimeout:3 handler:nil];
}

- (void)mergeBGChangesToMain:(NSNotification *)notification {
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.mainMOC mergeChangesFromContextDidSaveNotification:notification];

        // now after merge done, lets find our object with expected value `_expectedBGValue`:
        Earthquake * expectedEQ = [Earthquake MR_findFirstByAttribute:@"location" withValue:_expectedBGValue inContext:self.mainMOC];
        if (!expectedEQ){
            Earthquake * eqFirst = [Earthquake MR_findFirstInContext:self.mainMOC];
            NSLog(@"\nCurrent main MOC value is:\n%@\nexptected:\n%@", eqFirst.location, _expectedBGValue);
        }
        XCTAssert(expectedEQ != nil, @"Expected value not found");
        [_expectation fulfill];
    });
}

1 个答案:

答案 0 :(得分:4)

首先,在发布核心数据代码时,我建议您不要发布依赖于第三方库的代码,除非该第三方库与您的问题直接相关。我认为MR是一个神奇的记录,但我不会使用它,而且它似乎只是混淆了帖子的水域,因为谁知道它是什么(或不是)在幕后做什么。

换句话说,尝试将示例修剪为尽可能少的代码......并且不再需要......并且在绝对必要时仅包含第三方库。

其次,在为核心数据使用编写单元测试时,我建议使用内存堆栈。你总是空着,可以根据需要进行初始化。更容易用于测试。

那就是说,你的问题是对mergeChangesFromContextDidSaveNotification做什么(和不做)的误解。

基本上,您在Core Data持久性存储中有一个对象。您通过相同的PSC将两个不同的MOC连接到商店。

然后,您的测试将对象加载到主MOC中,并更改该值而不保存到PSC。然后第二个MOC加载相同的对象,并将其值更改为不同的值(即,存储,并且两个MOC对于同一对象的特定属性都具有不同的值)。

现在,当我们保存MOC时,如果存在冲突,将按照mergePolicy的指示处理冲突。但是,合并政策不适用于mergeChangesFromContextDidSaveNotification

您可以将mergeChangesFromContextDidSaveNotification视为插入任何新对象,删除任何已删除的对象,以及&#34;刷新&#34;任何更新的对象,同时保留任何本地更改。

在测试中,如果您添加其他属性(例如,&#34;标题&#34;)并更改&#34;标题&#34;和&#34;位置&#34;在BG MOC,但只改变&#34; location&#34;在主要的MOC中,你会看到&#34;标题&#34;按预期从BG MOC合并到主要MOC。

但是,正如您在问题中所述,&#34;位置&#34>似乎没有合并。实际上,它确实被合并了,但是任何本地更改都会覆盖商店中的内容...这正是您想要发生的事情,因为用户可能进行了更改,并且不希望它被更改在他们背后。

基本上,任何待定的本地更改都将覆盖要合并的MOC的更改。

如果你想要不同的东西,你必须在进行合并时实现这种行为,就像这样......

- (void)mergeBGChangesToMain:(NSNotification*)note {
    NSMutableSet *updatedObjectIDs = [NSMutableSet set];
    for (NSManagedObject *obj in [note.userInfo objectForKey:NSUpdatedObjectsKey]) {
        [updatedObjectIDs addObject:[obj objectID]];
    }

    [_mainMOC performBlock:^{
        for (NSManagedObject *obj in [_mainMOC updatedObjects]) {
            if ([updatedObjectIDs containsObject:obj.objectID]) {
                [_mainMOC refreshObject:obj mergeChanges:NO];
            }
        }

        [_mainMOC mergeChangesFromContextDidSaveNotification:note];
    }];
}

该代码首先收集在合并来自MOC中更新的每个对象的ObjectID

在进行合并之前,我们会查看merge-to-MOC中的每个更新对象。如果我们将一个对象合并到我们的MOC中,并且我们的合并到MOC也改变了该对象,那么我们希望允许合并来自MOC的值覆盖合并到MOC中的值。因此,我们从商店刷新本地对象,基本上丢弃任何本地更改(有副作用,例如,导致对象成为错误,释放对任何关系的引用,以及释放任何瞬态属性 - 请参阅refreshObject的文档:mergeChanges :)

考虑以下类别,它可以解决您的情况,以及使用NSFetchedResultsController等观察者时的常见问题。

@interface NSManagedObjectContext (WJHMerging)
- (void)mergeChangesIntoContext:(NSManagedObjectContext*)moc
    withDidSaveNotification:(NSNotification*)notification
    faultUpdatedObjects:(BOOL)faultUpdatedObjects
    overrideLocalChanges:(BOOL)overrideLocalChanges
    completion:(void(^)())completionBlock;
@end

@implementation NSManagedObjectContext (WJHMerging)
- (void)mergeChangesIntoContext:(NSManagedObjectContext *)moc
    withDidSaveNotification:(NSNotification *)notification
    faultUpdatedObjects:(BOOL)faultUpdatedObjects
    overrideLocalChanges:(BOOL)overrideLocalChanges
    completion:(void (^)())completionBlock {
    NSAssert(self == notification.object, @"Not called with");

    NSSet *updatedObjects = notification.userInfo[NSUpdatedObjectsKey];
    NSMutableSet *updatedObjectIDs = nil;
    if (overrideLocalChanges || faultUpdatedObjects) {
        updatedObjectIDs = [NSMutableSet setWithCapacity:updatedObjects.count];
        for (NSManagedObject *obj in updatedObjects) {
            [updatedObjectIDs addObject:[obj objectID]];
        }
    }

    [moc performBlock:^{
        if (overrideLocalChanges) {
            for (NSManagedObject *obj in [moc updatedObjects]) {
                if ([updatedObjectIDs containsObject:obj.objectID]) {
                    [moc refreshObject:obj mergeChanges:NO];
                }
            }
        }

        if (faultUpdatedObjects) {
            for (NSManagedObjectID *objectID in updatedObjectIDs) {
                [[moc objectWithID:objectID] willAccessValueForKey:nil];
            }
        }

        [moc mergeChangesFromContextDidSaveNotification:notification];

        if (completionBlock) {
            completionBlock();
        }
    }];
}
@end