ios - privateQueue managedObjectContext 的奇怪问题

标签 ios objective-c core-data concurrency batch-updates

SETUP(您可以稍后阅读并先跳到场景部分)

这是一个旧的应用程序,像这样手动设置 CoreData 堆栈:

+ (NSManagedObjectContext *)masterManagedObjectContext
{
    if (_masterManagedObjectContext) {
        return _masterManagedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self createPersistentStoreCoordinator];

    if (coordinator != nil) {
        _masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        _masterManagedObjectContext.retainsRegisteredObjects = YES;
        _masterManagedObjectContext.mergePolicy = NSOverwriteMergePolicy;
        _masterManagedObjectContext.persistentStoreCoordinator = coordinator;
    }
    return _masterManagedObjectContext;
}

+ (NSManagedObjectContext *)managedObjectContext
{
    if (_managedObjectContext) {
        return _managedObjectContext;
    }

    NSManagedObjectContext *masterContext = [self masterManagedObjectContext];

    if (masterContext) {
        _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        _managedObjectContext.retainsRegisteredObjects = YES;
        _managedObjectContext.mergePolicy = NSOverwriteMergePolicy;
        _managedObjectContext.parentContext = masterContext;
    }

    return _managedObjectContext;
}

+ (NSManagedObjectContext *)newManagedObjectContext
{
    __block NSManagedObjectContext *newContext = nil;
    NSManagedObjectContext *parentContext = [self managedObjectContext];

    if (parentContext) {
        newContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        newContext.parentContext = parentContext;
    }

    return newContext;
}

然后递归保存上下文:

+ (void)saveContext:(NSManagedObjectContext *)context
{
    [context performBlockAndWait:^{
        if (context.hasChanges && context.persistentStoreCoordinator.persistentStores.count) {
            NSError *error = nil;

            if ([context save:&error]) {
                NSLog(@"saved context: %@", context);

                // Recursive save parent context.
                if (context.parentContext) [self saveContext:context.parentContext];
            }
            else {
                // do some real error handling
                NSLog(@"Could not save master context due to %@", error);
            }
        }
    }];
}

场景

应用程序从服务器加载大量数据,然后在 newContext 中执行更新首先,然后合并到 mainContext -> masterContext -> persistentStore .

因为数据量很大,所以同步进程被分成了大约 10 个异步线程 => 我们有 10 个 newContext一次。

现在,数据很复杂,像parents <-> children (same class)这样的东西. 1 parent可以有很多children , 和一个 child可以有一个 mother, father, god father, step mother... , 所以它是 n-n relationship .首先,我们获取 parent , 然后执行 fetch child然后设置 childparent , 等等。

服务器有点笨,它不能发送禁用的对象。但是,客户希望从后端控制应用程序对象的显示,所以我有 2 个属性可以做到这一点:

  1. hasUpdated : 在加载过程开始时,执行批量更新,设置所有对象的hasUpdated否。从服务器获取数据后,将此属性更新为 YES。
  2. isActive : 当所有加载完成后,如果 hasUpdate == NO 执行批量更新此属性为 NO .然后,我有一个过滤器不会显示带有 isActive == NO 的对象

问题

客户提示为什么一些对象即使在后端启用了也会丢失。在遇到这个奇怪的问题后,我已经挣扎和调试了很长时间:

  1. newContext.updatedObjects : { obj1.ID = 100, hasUpdated == YES
  2. “保存的新上下文”
  3. mainContext.updatedObjects:{obj1.ID = 100,hasUpdated == NO

//我会在这里停下来。显然,master 得到了更新 = NO 最后 isActive将设置为 no,这会导致丢失对象。

如果它每次都发生,那么可能更容易修复(也许?)。然而,它是这样发生的:

  • 第一次运行(第一次,我的意思是应用程序从调用 appDidFinishLaunch... 的地方开始):完全正确
  • 第二次:丢失(153 个对象)
  • 第三次:全部正确
  • 第 4 次:丢失(153 个对象)(再次?正是那些有多个 parent 的对象,我相信是这样!)
  • 第5次:再次纠正
  • ...等等。

此外,对于具有相同上下文(相同的 newContext )的对象来说,这似乎发生了。难以置信。

问题

为什么会这样?我该如何解决?如果那些对象没有 child ,我的生活会更轻松!!!!

奖金

如果你想知道批量更新是如何进行的,请看下面。注意:

  1. 下载请求在异步队列中:_shareInstance.apiQueue = dispatch_queue_create("product_request_queue", DISPATCH_QUEUE_CONCURRENT);
  2. 解析响应和更新属性在队列中是同步的:_shareInstance.saveQueue = dispatch_queue_create("product_save_queue", DISPATCH_QUEUE_SERIAL);
  3. 每当解析完成时,我都会执行保存 newContext并调用updateProductActiveStatus:在同一个串行队列中。如果所有请求都完成,则执行批量更新状态。由于请求是在并发队列中完成的,它总是比保存(串行)队列更早完成,所以这是一个非常简单的过程。

代码:

// Load Manager
- (void)resetProductUpdatedStatus
{
    NSBatchUpdateRequest *request = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:NSStringFromClass([Product class])];
    request.propertiesToUpdate = @{ @"hasUpdated" : @(NO) };
    request.resultType = NSUpdatedObjectsCountResultType;

    NSBatchUpdateResult *result = (NSBatchUpdateResult *)[self.masterContext executeRequest:request error:nil];

    NSLog(@"Batch update hasUpdated: %@", result.result);

    [self.masterContext performBlockAndWait:^{
        [self.masterContext refreshAllObjects];

        [[CoreDataUtil managedObjectContext] performBlockAndWait:^{
            [[CoreDataUtil managedObjectContext] refreshAllObjects];
        }];
    }];
}

- (void)updateProductActiveStatus:(SyncComplete)callback
{
    if (self.apiRequestList.count) return;

    NSBatchUpdateRequest *request = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:NSStringFromClass([Product class])];
    request.predicate = [NSPredicate predicateWithFormat:@"hasUpdated = NO AND isActive = YES"];
    request.propertiesToUpdate = @{ @"isActive" : @(NO) };
    request.resultType = NSUpdatedObjectsCountResultType;

    NSBatchUpdateResult *result = (NSBatchUpdateResult *)[self.masterContext executeRequest:request error:nil];
    NSLog(@"Batch update isActive: %@", result.result);

    [self.masterContext performBlockAndWait:^{
        [self.masterContext refreshAllObjects];

        NSManagedObjectContext *maincontext = [CoreDataUtil managedObjectContext];
        NSLog(@"Refreshed master");

        [maincontext performBlockAndWait:^{
            [maincontext refreshAllObjects];

            NSLog(@"Refreshed main");

            // Callback
            if (callback) dispatch_async(dispatch_get_main_queue(), ^{ callback(YES, nil); });
        }];
    }];
}

最佳答案

mergePolicy 是邪恶的。唯一正确的 mergePolicy 是 NSErrorMergePolicy 任何其他策略都是要求核心数据静默失败,而不是在您期望的时候更新。

我怀疑您的问题是您正在使用背景上下文同时写入核心数据。 (我知道你说你有一个串行队列 - 但如果你在队列中调用 performBlock 那么每个 block 都会同时执行)。当有冲突时,东西会被覆盖。您应该只以一种同步方式写入核心数据。

我写了一篇关于如何使用 NSPersistentContainer 完成此操作的答案: NSPersistentContainer concurrency for saving to core data我建议您将代码迁移到它。这真的不应该那么难。

如果您想让代码尽可能接近当前的代码,那也不难。

制作串行操作队列:

_persistentContainerQueue = [[NSOperationQueue alloc] init];
_persistentContainerQueue.maxConcurrentOperationCount = 1;

并使用此队列进行所有写入:

- (void)enqueueCoreDataBlock:(void (^)(NSManagedObjectContext* context))block{
    void (^blockCopy)(NSManagedObjectContext*) = [block copy];

    [self.persistentContainerQueue addOperation:[NSBlockOperation blockOperationWithBlock:^{
        NSManagedObjectContext* context =  [CoreDataUtil newManagedObjectContext];
        [context performBlockAndWait:^{
            blockCopy(context);
            [CoreDataUtil saveContext:context];
        }];
    }]];
}

也可能是对象已更新,但您没有看到它,因为您依赖于更新的 fetchedResultsController。而且 fetchedResultsController 不会根据批量更新请求进行更新。

关于ios - privateQueue managedObjectContext 的奇怪问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45467739/

相关文章:

ios - GigyaSwift 登录方法未打开 FacebookWrapper

IOS - 绘制圆角正方形

ios - 在 swift iOS 8 中单击单元格时,如何让它打开另一个 View Controller ?

ios - 传递给另一个类时,NSString 的 id 返回 null

macos - 核心数据崩溃报告: sqlite3VdbeHalt in loading array controller

IOS:proposalCredentials 和 URLCredential 之间的信任差异

objective-c - NSNumbers 比 int 占用更少的内存?

ios - 如何调整调整 UICollectionView 大小的 Autolayout 位置?

objective-c - ManagedObjectContext 从 AppDelegate 返回 nil

ios - 下载初始应用程序数据