Как я могу гарантировать уникальные записи в хранилище основных данных в общем контейнере приложения, используемом как хост-приложением, так и расширением?
Чтобы эффективно задать вопрос, позвольте сначала рассмотреть точный сценарий, с которым я сталкиваюсь:
Общая настройка
- Приложение хоста iOS 8.
- Один или несколько расширений iOS 8 (WatchKit, Share и т.д.) в комплекте с хост-приложением.
- Приложение-хост и все расширения совместно используют одно хранилище данных SQLite в общем контейнере группы приложений.
- Каждое приложение/расширение имеет собственный NSPsistentStoreCoordinator и NSManagedObjectContext.
- Каждый постоянный координатор хранилища использует постоянное хранилище, которое использует те же ресурсы SQLite в контейнере группы, что и все остальные постоянные хранилища.
- Приложение и все расширения используют общую кодовую базу для синхронизации содержимого с удаленного ресурса API в Интернете.
Последовательность событий, ведущих к проблеме
-
Пользователь запускает хост-приложение. Он начинает получать данные из удаленного ресурса API. Объекты модели основных данных создаются на основе ответа API и "обновлены" в контексте объекта управляемого приложения. Каждый объект API имеет уникальный идентификатор, который идентифицирует его в удаленном интерфейсе API. Под "upsert" я подразумеваю, что для каждого объекта API приложение-хозяин создает новую запись в Core Data, если существующая запись для данного уникального идентификатора не может быть найдена.
-
Между тем пользователь также запускает одно из расширений хост-приложений. Он также выполняет какую-то выборку из того же удаленного API. Он также пытается выполнить "upsert" при анализе ответов API.
-
Проблема: Что произойдет, если и хост-приложение, и расширение попытаются одновременно обновить запись Core Data для одного и того же объекта API? Чтобы узнать, как это может произойти, давайте посмотрим на последовательность событий для upsert:
Последовательность обновления основных данных:
- Код анализа API анализирует уникальный идентификатор для данного объекта API.
- Анализатор выполняет выборку основных данных для любой записи, которая соответствует предикату, где
uniqueID
равна разобранному уникальному идентификатору.
- Если существующая запись не найдена, анализатор вставляет новую запись Core Data для этого объекта API, устанавливает свой атрибут
uniqueID
в разобранный уникальный идентификатор.
- Анализатор сохраняет контекст управляемого объекта, который подталкивает новые данные ввода в хранилище резервных копий SQLite.
Проблема в деталях
Предположим, что приложение-хозяин и расширение самостоятельно анализируют ответ API для одного и того же объекта API в одно и то же время. Если и хост-приложение, и расширение достигнут шага 3 до того, как любой из них завершит Шаг 4, они оба попытаются вставить новую запись Core Data для того же уникального идентификатора. Когда они достигнут шага 4 и вызовут save:
в соответствующих контекстах управляемых объектов, Core Data с радостью создаст повторяющиеся записи.
Насколько мне известно, Core Data не имеет возможности пометить атрибут как уникальный. Мне нужен Core Data эквивалент SQLite INSERT OR IGNORE
+ UPDATE
комбо.. Или мне нужен способ "заблокировать" хранилище резервных хранилищ SQLite, которое звучит как рецепт проблемы.
Есть ли известный подход к этой довольно новой проблеме, введенной расширениями iOS 8?
Ответы
Ответ 1
Есть ли известный подход к этой довольно новой проблеме, введенной расширениями iOS 8?
Да, это тот же подход, который применяется при использовании iCloud с Core Data: пусть дубликаты происходят, но затем идут и очищают их. В обеих ситуациях существует риск создания повторяющихся записей, и нет абсолютно надежного способа их предотвращения. Поскольку у вас есть ключ uniqueID
, вы в хорошей форме, насколько это возможно.
Было бы намного проще, как отмечает Дэйв ДеЛонг, во избежание проблемы в первую очередь. Если это невозможно, вы можете справиться с этим, с некоторой дополнительной работой.
Поиск дубликатов будет примерно таким:
NSError *error = nil;
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] init];
[moc setPersistentStoreCoordinator:self.persistentStoreCoordinator];
NSFetchRequest *fr = [[NSFetchRequest alloc] initWithEntityName:@"MyEntityName"];
[fr setIncludesPendingChanges:NO];
NSExpression *countExpr = [NSExpression expressionWithFormat:@"count:(uniqueID)"];
NSExpressionDescription *countExprDesc = [[NSExpressionDescription alloc] init];
[countExprDesc setName:@"count"];
[countExprDesc setExpression:countExpr];
[countExprDesc setExpressionResultType:NSInteger64AttributeType];
NSAttributeDescription *uniqueIDAttr = [[[[[_psc managedObjectModel] entitiesByName] objectForKey:@"MyEntityName"] propertiesByName] objectForKey:@"uniqueID"];
[fr setPropertiesToFetch:[NSArray arrayWithObjects:uniqueIDAttr, countExprDesc, nil]];
[fr setPropertiesToGroupBy:[NSArray arrayWithObject:uniqueIDAttr]];
[fr setResultType:NSDictionaryResultType];
NSArray *countDictionaries = [moc executeFetchRequest:fr error:&error];
Это почти эквивалент Core Data, аналогичный этому в SQL:
SELECT uniqueID, COUNT(uniqueID) FROM MyEntityName GROUP BY uniqueID;
Вы получаете массив словарей, каждый из которых содержит uniqueID
и подсчет количества раз, когда это значение используется. Запустите словарь и обработайте дубликаты соответствующим образом.
Я описал это более подробно в сообщении в блоге. Также есть образец проекта от Apple, который демонстрирует процесс, называемый SharedCoreData, но я считаю, что он доступен только в составе образца кода WWDC 2012. Это также было описано на сессии 227 на этой конференции.
Ответ 2
Похоже, что самым простым подходом к этому было бы просто избежать множественных писателей в первую очередь. Почему бы не просто полностью отключить свои расширения от кэшированных данных, а затем обновить хранилище данных только из основного приложения iOS?