Импорт данных многопоточности данных (повторяющиеся объекты)

У меня есть NSOperationQueue, который импортирует объекты в Core Data, которые я получаю из веб-api. Каждая операция имеет частный дочерний элемент managedObjectContext моего основного управляемого объекта appObjectContext. Каждая операция принимает объект для импорта и проверяет, существует ли объект, в каком случае он обновляет существующий объект. Если объект не существует, он создает этот новый объект. Эти изменения в частных дочерних контекстах затем распространяются до основного контекста управляемых объектов.

Эта настройка работала очень хорошо для меня, , но проблема с дубликатами.

Когда у меня есть тот же объект, который импортируется в двух разных параллельных операциях, я получаю повторяющиеся объекты с одинаковыми данными. (Оба они проверяют, существует ли объект, и он не кажется им уже существующим). Причина, по которой у меня будет 2 из тех же объектов, импортирующих примерно в одно и то же время, заключается в том, что я часто обрабатываю "новый" вызов api, а также "get" api call. Из-за одновременного асинхронного характера моей установки трудно убедиться, что у меня никогда не будет повторяющихся объектов, пытающихся импортировать.

Итак, мой вопрос - лучший способ решить эту проблему? Я думал об ограничении импорта до максимальных параллельных операций до 1 (мне это не нравится из-за производительности). Аналогично, я рассматриваю необходимость сохранения после каждой операции импорта и попытки обработки слияния контекстов. Кроме того, я посчитал, что впоследствии собираю данные, чтобы иногда очищать дубликаты. И, наконец, я рассмотрел просто обработку дубликатов во всех запросах на выборку. Но ни одно из этих решений не кажется мне большим, и, возможно, есть легкое решение, которое я рассмотрел.

Ответ 1

Итак, проблема такова:

  • Контексты
  • представляют собой блокнот - если и до тех пор, пока вы не сохраните, изменения, внесенные вами в них, не будут перенесены в постоянное хранилище;
  • вы хотите, чтобы один контекст знал о внесенных изменениях, которые еще не были нажаты.

Мне не кажется, что слияние между контекстами будет работать - контексты не являются потокобезопасными. Поэтому для слияния ничего больше не может продолжаться в потоке/очереди другого контекста. Поэтому вы никогда не сможете устранить риск того, что новый объект будет вставлен, в то время как другой контекст частично пройдет через процесс вставки.

Дополнительные наблюдения:

  • SQLite не является потокобезопасным в практическом смысле;
  • следовательно, все поездки в постоянное хранилище сериализуются независимо от способа их выпуска.

Принимая во внимание проблему и ограничения SQLite, в моем приложении мы приняли структуру, в соответствии с которой веб-вызовы естественно параллельны по NSURLConnection, последующий анализ результатов (анализ JSON плюс некоторый промысел в результате) происходит одновременно, а затем шаг поиска или создания направляется в последовательную очередь.

Очень мало времени обработки теряется в результате сериализации, потому что поездки SQLite будут сериализованы в любом случае, и они являются подавляющим большинством сериализованных материалов.

Ответ 2

Начните с создания зависимостей между вашими операциями. Убедитесь, что вы не можете завершить работу до тех пор, пока это не произойдет.

Отъезд http://developer.apple.com/library/mac/documentation/Cocoa/Reference/NSOperation_class/Reference/Reference.html#//apple_ref/occ/instm/NSOperation/addDependency:

Каждая операция должна вызывать сохранение по завершении. Затем я попробую предложенную здесь методологию Find-Or-Create:

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreData/Articles/cdImporting.html

Он решит вашу проблему с дубликатами и, вероятно, может привести к меньшему количеству извлечений (которые дороги и медленны, поэтому быстро разряжайте аккумулятор).

Вы также можете создать глобальный дочерний контекст для обработки всего вашего импорта, а затем объединить всю огромную вещь в конце, но это действительно сводится к тому, насколько большой набор данных и соображения вашей памяти.

Ответ 3

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

Обратите внимание, что это по сути непроверено, так как в моем случае я редко вижу эту повторяющуюся проблему очень редко во время тестирования, и нет очевидного способа легко воспроизвести ее.

У меня такая же установка стека CoreData - мастер MOC в частной очереди, у которой есть дочерний элемент в главной очереди, и он используется в качестве основного контекста приложения. Наконец, операции массового импорта (find-or-create) передаются на третий MOC с использованием фоновой очереди. Как только операция завершена, сохраняются данные до PSC.

Я перенес весь стек Core Data из AppDelegate в отдельный класс (AppModel), который предоставляет приложению доступ к агрегированному корневому объекту домена (Player), а также вспомогательную функцию для выполнение фоновых операций над моделью (performBlock:onSuccess:onError:).

К счастью для меня, все основные операции CoreData проходят через этот метод, поэтому, если я могу гарантировать, что эти операции будут выполняться последовательно, то проблема с дублированием должна быть решена.

- (void) performBlock: (void(^)(Player *player, NSManagedObjectContext *managedObjectContext)) operation onSuccess: (void(^)()) successCallback onError:(void(^)(id error)) errorCallback
{
    //Add this operation to the NSOperationQueue to ensure that 
    //duplicate records are not created in a multi-threaded environment
    [self.operationQueue addOperationWithBlock:^{

        NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        [managedObjectContext setUndoManager:nil];
        [managedObjectContext setParentContext:self.mainManagedObjectContext];

        [managedObjectContext performBlockAndWait:^{

            //Retrive a copy of the Player object attached to the new context
            id player = [managedObjectContext objectWithID:[self.player objectID]];
            //Execute the block operation
            operation(player, managedObjectContext);

            NSError *error = nil;
            if (![managedObjectContext save:&error])
            {
                //Call the error handler
                dispatch_async(dispatch_get_main_queue(), ^{
                    NSLog(@"%@", error);
                    if(errorCallback) return errorCallback(error);
                });
                return;
            }

            //Save the parent MOC (mainManagedObjectContext) - WILL BLOCK MAIN THREAD BREIFLY
            [managedObjectContext.parentContext performBlockAndWait:^{
                NSError *error = nil;
                if (![managedObjectContext.parentContext save:&error])
                {
                    //Call the error handler
                    dispatch_async(dispatch_get_main_queue(), ^{
                        NSLog(@"%@", error);
                        if(errorCallback) return errorCallback(error);
                    });
                    return;
                }
            }];

            //Attempt to clear any retain cycles created during operation
            [managedObjectContext reset];

            //Call the success handler
            dispatch_async(dispatch_get_main_queue(), ^{
                if (successCallback) return successCallback();
            });
        }];
    }];
}

Что я добавил здесь, я надеюсь, что это решит проблему для меня, это обернуть все это в addOperationWithBlock. Моя очередь операций просто настроена следующим образом:

single.operationQueue = [[NSOperationQueue alloc] init];
[single.operationQueue setMaxConcurrentOperationCount:1];

В моем классе API я могу выполнить импорт в своей операции следующим образом:

- (void) importUpdates: (id) methodResult onSuccess: (void (^)()) successCallback onError: (void (^)(id error)) errorCallback
{
    [_model performBlock:^(Player *player, NSManagedObjectContext *managedObjectContext) {
        //Perform bulk import for data in methodResult using the provided managedObjectContext
    } onSuccess:^{
        //Call the success handler
        dispatch_async(dispatch_get_main_queue(), ^{
            if (successCallback) return successCallback();
        });
    } onError:errorCallback];
}

Теперь с NSOperationQueue на месте больше не может быть возможным одновременное выполнение нескольких партийных операций.