【问题标题】:NSManagedObjectContext in inconsistent state after mergeChangesFromContextDidSaveNotification in background contextNSManagedObjectContext 在后台上下文中 mergeChangesFromContextDidSaveNotification 后处于不一致状态
【发布时间】:2013-05-07 21:56:16
【问题描述】:

我在 CoreData 中遇到了一些奇怪的行为,导致我的一个 MOC 最终处于不一致的状态。我已经在一个小样本中复制了这个问题project

这是我的情况的基本概述:

  • 我有两种实体类型,管道和盒子。每个管道可以包含 0 个或多个盒子,每个盒子都是一个管道的一部分

  • 在我的示例项目中:

    • 我正在创建 1 个管道和 3 个样本框,它们都指向该管道。
    • 此示例数据是在使用 NSMainQueueConcurrencyType 创建的 MOC 上创建的
    • 我创建了一个后台 MOC (`NSPrivateQueueConcurrencyType') 并获取所有框并仅删除其中一个。
    • 此删除导致管道更新(关系中应该少一个框)
    • 当我保存后台 MOC 时,我尝试将更改合并到主队列 MOC 中

问题是在合并之后,主队列上下文成功合并了删除但没有将编辑合并到管道中,这应该表明关系中少了一个框。

不知何故,它没有合并整个变化。

以下是部分代码:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{

    // create a core database and a moc on the ui thread
    [self initCoreData];

    // fill up db with dummy data all on the main thread
    [self createDummyData:self.uiContext];

    [self printUIContextContents];

    // now create a background moc
    NSManagedObjectContext* backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];

    // when the backgrouynd context saves, merge changees in to UI context
    [backgroundContext performBlockAndWait:^{
        backgroundContext.mergePolicy = [[NSMergePolicy alloc] initWithMergeType:NSMergeByPropertyObjectTrumpMergePolicyType];
        [backgroundContext setPersistentStoreCoordinator:self.persistentCoordinator];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mocDidSave:) name:NSManagedObjectContextDidSaveNotification object:backgroundContext];
    }];


    // now delete a box on background thread
    [backgroundContext performBlock:^{
        NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Box"];
        request.predicate = [NSPredicate predicateWithFormat:@"name = %@", @"box1"];

        NSError* error = nil;
        NSArray* boxes = [backgroundContext executeFetchRequest:request error:nil];
        if (error != nil){
            NSLog(@"Error in deleting box: %@", error);
        }

        Box* box = (Box*)boxes[0];

        [backgroundContext deleteObject:box];
        [backgroundContext save:nil];
    }];

    [self printUIContextContents];


    return YES;
}

- (void) printUIContextContents {
    [self.uiContext performBlockAndWait:^{
        NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Pipeline"];
        NSArray* pipelines = [self.uiContext executeFetchRequest:request error:nil];
        for (Pipeline* pipeline in pipelines) {
            NSLog(@"Pipeline Name: %@", pipeline.name);
            NSLog(@"\tBoxes that are in the pipeline relationship: ");
            for (Box* box in pipeline.boxes) {
                NSLog(@"\t\t%@", box.name);
            }
        }

        NSLog(@" ");

        request = [NSFetchRequest fetchRequestWithEntityName:@"Box"];
        NSArray* boxes = [self.uiContext executeFetchRequest:request error:nil];
        NSLog(@"All Boxes Entities Present:");
        for (Box* box in boxes) {
            NSLog(@"\t%@", box.name);
        }
        NSLog(@" ");
        NSLog(@" ");
    }];
}

- (void)mocDidSave:(NSNotification *)notif {

    [self.uiContext performBlockAndWait:^(void) {
        [self.uiContext mergeChangesFromContextDidSaveNotification:notif];
    }];
}


- (void) createDummyData:(NSManagedObjectContext*)context {
    NSArray* boxNames = [NSArray arrayWithObjects:@"box1", @"box2", @"box3", nil];
    NSString* pipelineName = @"pipeline1";

    [self.uiContext performBlockAndWait:^{
        Pipeline* pipe = (Pipeline*)[self createEntity:@"Pipeline" inContext:self.uiContext];
        pipe.name = pipelineName;

        for (NSString* boxName in boxNames) {
            Box* box = (Box*)[self createEntity:@"Box" inContext:self.uiContext];
            box.name = boxName;
            box.pipeline = pipe;
        }

        NSError* error = nil;
        [self.uiContext save:&error];
        if (error != nil){
            NSLog(@"Error in create dummy data: %@", error);
        }
    }];
}


- (void) initCoreData {
    NSURL* modelURL = [[NSBundle mainBundle] URLForResource:@"Model" withExtension:@"momd"];
    NSURL* storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"StreakDB.sqlite"];
    [[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil];

    NSManagedObjectModel* objectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];

    NSError *error = nil;
    self.persistentCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:objectModel];
    if (![self.persistentCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
        /*
         SOME ERROR HANDLING HERE
         */
    }

    self.uiContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    [self.uiContext setPersistentStoreCoordinator:self.persistentCoordinator];
    self.uiContext.mergePolicy = [[NSMergePolicy alloc] initWithMergeType:NSMergeByPropertyObjectTrumpMergePolicyType];
}

- (NSManagedObject*)createEntity:(NSString *)entityType inContext:(NSManagedObjectContext *)context {
    return [NSEntityDescription insertNewObjectForEntityForName:entityType
                                         inManagedObjectContext:context];
}

任何想法可能会发生什么?同样,这里有一个示例项目来说明问题(并打印出结果):project

更新:

管道名称:pipeline1 处于管道关系中的框: 盒子3 盒子1 盒子2

存在的所有框实体: 盒子3 盒子2 盒子1

管道名称:pipeline1 处于管道关系中的框: 盒子3 盒子1 盒子2

存在的所有框实体: 盒子3 盒子2

如您所见,当我第二次打印出 uiContext 时,它处于不一致的状态。具体来说,上下文中有 2 个框,但管道具有指向 3 个框的关系 - 因此不一致。

我知道后台保存可能在第二次打印之前或之后完成,但在任何一种情况下,上下文的状态都应该是一致的,不是吗? (即关系中的 3 个框和 3 个项目或关系中的 2 个框和 2 个项目)。

【问题讨论】:

  • 尝试使背景上下文成为 UI 上下文的智利上下文,而不是观察和合并。还要确保在模型中的关系上正确设置了级联和 nil 规则。
  • 如果我使用父子上下文它确实有效,但我想知道为什么合并不起作用。我觉得我缺少一些基本的核心数据概念。
  • 删除规则全部设置正确。

标签: ios core-data merge nsmanagedobjectcontext


【解决方案1】:

这可能是线程问题(您正在并行执行 BG 操作)。尝试将其更改为:[backgroundContext performBlockAndWait: ...];

阐述:
上下文未处于不一致状态。
为什么?

0:每个 fetch 请求都是一次存储,并且不依赖于执行时刻上下文包含的数据,检索到的对象与上下文的现有行缓存匹配,以便重用现有商品信息并防止在上下文中重复商品,您可以获得当前商店状态的快照。

实际情况是:
1. 您在 MOC1(主上下文)中创建项目,然后使用 2 阶段获取请求记录对象。
1.1。由于 (0),如果底层存储在两次提取之间发生变化,则每个提取请求可能会返回不同的数据集
2. 您创建的 MOC2 使用另一个线程执行其操作,但它的创建阻塞了 MOC1 的线程(主线程)。
2.1。您使用 MOC2 线程执行异步块。
2.2. MOC2 正在删除一些对象
2.3. MOC2 保存其更改(商店在此处更改,在合并之前)
2.4. MOC2 正在尝试合并对 MOC1 的更改,但由于 MOC1 的线程正忙于执行您的日志功能(请参阅 (3.))。
3。请注意,主线程现在再次进入您的日志函数,并且当前运行循环尚未退出(与 2.1. 操作并行),因此在主运行循环完成其循环之前无法合并到 MOC1。
3.1。 MOC1 执行日志功能第一次获取请求(很可能,在 MOC2 有更改以将其更改保存到存储之前,此请求将阻塞协调器,因此即使 MOC2 准备保存,协调器也会被阻塞,直到第一次获取已完成。
3.1.1。你得到 box1,box2,box3 3.2. MOC1 执行第二次获取请求(很可能,在 MOC2 保存到存储之后) 3.2.1。你得到box2,box3 4. main runloop end 和 MOC2 现在可以将其更改合并到 MOC1

希望这能澄清一点。

在您合并对 MOC1 的更改后尝试记录,看看情况是否正常。

在处理多个线程时,您需要通过使用通知(如您所做的那样)或使用父子架构来同步您的 MOC。

在父子架构中,这不会发生,但实际写入存储的上下文将是主上下文,这将阻塞您的主线程。

【讨论】:

  • 这当然有效。这是一个虚拟应用程序,因此在这里没有什么区别。但是在我的真实应用程序中,我不想 performBlockAndWait 否则我的主线程会阻塞,直到操作完成。我想在后台执行保存,然后在完成后将更改合并到 uiContext 中。
  • 这正是您应该期待的结果。第二次验证中的第一次取指是在 BG 上下文保存之前完成的,第二次取指是在保存之后完成的。如果你使用 FRC,你会得到一致的结果,因为它只会在合并到主上下文后更新。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-08-03
  • 1970-01-01
  • 2015-12-14
相关资源
最近更新 更多