问题
To ask my question effectively, let's first consider the exact scenario I'm facing:
General Setup
- A host iOS 8 app.
- One or more iOS 8 extensions (WatchKit, Share, etc.) bundled with the host app.
- The host app and all extensions share the same Core Data SQLite store in the shared app group container.
- Each app/extension has its own NSPersistentStoreCoordinator and NSManagedObjectContext.
- Each persistent store coordinator uses a persistent store that shares the same SQLite resources in the group container as all the other persistent stores.
- The app and all extensions use a common codebase for syncing content from a remote API resource on the Internet.
Sequence of Events Leading to the Problem
The user launches the host app. It begins fetching data from the remote API resource. Core data model objects are created based on the API response and "upserted" into the host app's managed object context. Each API entity has a uniqueID that identifies it in the remote API backend. By "upsert," I mean that for each API entity, the host app only creates a new entry in Core Data if an existing entry for a given uniqueID cannot be found.
Meanwhile, the user also launches one of the host app's extensions. It, too, performs some kind of fetch from the same remote API. It also attempts to perform an "upsert" when parsing the API responses.
The Problem: What happens if both the host app and an extension try to upsert a Core Data entry for the same API entity at the same time? To see how this could come about, let's look at the sequence of events for an upsert:
Core Data Upsert Sequence:
- The API parsing code parses the uniqueID for a given API entity.
- The parser performs a Core Data fetch for any entry that matches a predicate where
uniqueID
is equal to the parsed uniqueID. - If an existing entry is not found, the parser inserts a new Core Data entry for this API entity, set's its
uniqueID
attribute to the parsed uniqueID. - The parser saves the managed object context, which pushes the new entry data down to the SQLite backing store.
Problem in Detail
Let's assume the host app and the extension are independently parsing an API response for the same API entity at the same time. If both the host app and an extension reach Step 3 before either of them has finished Step 4, then they will both be trying to insert a new Core Data entry for the same uniqueID. When they reach Step 4 and call save:
on their respective managed object contexts, Core Data will happily create duplicate entries.
As far as I'm aware, Core Data doesn't have any way to mark an attribute as unique. I need a Core Data equivalent to a SQLite INSERT OR IGNORE + UPDATE combo.. Or else I need a way to "lock" the persistent store's SQLite backing store, which sounds like a recipe for trouble.
Is there a known approach to this rather novel problem introduced by iOS 8 extensions?
回答1:
Is there a known approach to this rather novel problem introduced by iOS 8 extensions?
Yes, it's the same approach that applies when using iCloud with Core Data: let the duplicates happen, but then go and clean them up. Both situations run the risk of creating duplicate entries, and there's no completely reliable way to prevent them. Since you have a uniqueID
key, you're in good shape as far as this is concerned.
It would be a lot easier, as Dave DeLong notes, to avoid the problem in the first place. If that's impossible, you can deal with it, with some extra work.
Finding duplicates would be something like:
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];
This is pretty much the Core Data equivalent of something like this in SQL:
SELECT uniqueID, COUNT(uniqueID) FROM MyEntityName GROUP BY uniqueID;
You get an array of dictionaries, each of which contains a uniqueID
and a count of the number of times that value is used. Run through the dictionary and deal with duplicates appropriately.
I described this in more detail in a blog post. There's also a sample project from Apple that demonstrates the process, called SharedCoreData, but I believe it's only available as part of the WWDC 2012 sample code bundle. It was also described in session 227 at that conference.
回答2:
It seems like the simplest approach to this would be to simply avoid the multiple writers in the first place. Why not just drive your extensions entirely off cached data, and then only update your data store from your primary iOS app?
来源:https://stackoverflow.com/questions/27020197/how-can-i-guarantee-unique-entries-in-a-core-data-store-in-a-shared-app-containe