Странная проблема с privateQueue manageObjectContext

НАСТРОЙКА (Вы можете прочитать это позже и сначала перейти к разделу сценариев)

Это старое приложение с ручной настройкой стека 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, затем выполняем выборку child, затем устанавливаем child в parent и так далее.

Сервер какой-то глупый, он не может отправлять отключенные объекты. Однако клиент хотел бы контролировать отображение объектов приложения из серверной части, поэтому у меня есть 2 свойства для этого:

  1. hasUpdated: В начале процесса загрузки выполните пакетное обновление, установите для всех объектов hasUpdated значение НЕТ. Получив данные с сервера, обновите это свойство до YES.
  2. isActive: Когда вся загрузка завершена, выполните пакетное обновление этого свойства до NO, если hasUpdate == 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...): все правильно
  • 2-й раз: отсутствует (153 объекта)
  • 3-й раз: все правильно
  • 4-й раз: отсутствует (153 объекта) (опять же? именно те, у которых несколько родителей, я так думаю!)
  • 5-й раз: снова правильно
  • ... so on.

Кроме того, похоже, что это произошло для объектов, которые имеют одинаковый контекст (один и тот же newContext). Невероятный.

ВОПРОСЫ

Почему это происходит? Как это исправить? Если бы у этих объектов не было детей, моя жизнь была бы проще!!!!

БОНУС

Если вы хотите узнать, как происходит пакетное обновление, оно приведено ниже. Примечание:

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

person Eddie    schedule 02.08.2017    source источник


Ответы (1)


mergePolicy зло. Единственная правильная mergePolicy — это NSErrorMergePolicy любая другая политика требует, чтобы основные данные молча терпели неудачу и не обновлялись, когда вы этого ожидаете.

Я подозреваю, что ваша проблема в том, что вы пишете одновременно в core-data с фоновыми контекстами. (Я знаю, что вы говорите, что у вас есть последовательная очередь, но если вы вызываете PerformBlock внутри очереди, то каждый блок выполняется одновременно). Когда есть конфликт, вещи перезаписываются. Вы должны записывать данные ядра только одним синхронным способом.

Я написал ответ о том, как это сделать с помощью NSPersistentContainer: concurrency для сохранения в основные данные, и я бы посоветовал вам перенести свой код на него. Это действительно не должно быть так сложно.

Если вы хотите, чтобы код был как можно ближе к тому, что есть на данный момент, это тоже не так уж сложно.

Создайте очередь последовательных операций:

_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 не обновляется из запросов на пакетное обновление.

person Jon Rose    schedule 03.08.2017
comment
Спасибо за ваш ответ, я попытаюсь перенести это и сообщить вам результат! В качестве вашего последнего комментария я на 100% уверен, что это не так. Список данных обновился правильно, поэтому клиенты жалуются, что данные появляются и исчезают после синхронизации. Кроме того, не могли бы вы рассказать мне, как бороться с NSErrorMergePolicy? Не попадет ли он в ветку ошибок [context save:&error]?? - person Eddie; 03.08.2017
comment
Через несколько дней (почти полмесяца) борьбы с этой проблемой я сдался. Сделайте простую миграцию с помощью MagicalRecord и выполните пакетное обновление, как в старые добрые времена: используя NSFetchRequest, выполните выборку и сохраните. Как-то до сих пор пользуюсь вашим enqueueCoreDataBlock:, чтобы делать все обновления и сохранения. Спасибо за вашу работу :( - person Eddie; 15.08.2017