Импорт данных многопоточности данных (повторяющиеся объекты)
У меня есть 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
на месте больше не может быть возможным одновременное выполнение нескольких партийных операций.