如何在由主应用程序和扩展程序共享的应用程序容器中,保证Core Data存储中的条目唯一?

19
为了有效地提出我的问题,让我们首先考虑我面临的确切情况:
一款宿主iOS 8应用程序。
一个或多个iOS 8扩展(WatchKit、Share等)与宿主应用程序捆绑在一起。
宿主应用程序和所有扩展共享相同的Core Data SQLite存储在共享应用程序组容器中。
每个应用程序/扩展都有自己的NSPersistentStoreCoordinator和NSManagedObjectContext。
每个持久性存储协调器使用一个持久性存储,该持久性存储与所有其他持久性存储共享组容器中的相同SQLite资源。
应用程序和所有扩展使用一个通用代码库,用于从Internet上的远程API资源同步内容。
导致问题的事件序列。
  1. 用户启动主机应用程序。它开始从远程API资源获取数据。根据API响应创建核心数据模型对象,并将其“upserted”到主机应用程序的托管对象上下文中。每个API实体都有一个唯一ID,用于在远程API后端中标识它。通过“upsert”,我指的是对于每个API实体,只有在找不到给定唯一ID的现有条目时,主机应用程序才会在Core Data中创建新条目。

  2. 同时,用户还启动了主机应用程序的其中一个扩展程序。它也从同一个远程API执行某种类型的提取。在解析API响应时,它还尝试执行“upsert”操作。

  3. 问题:如果主机应用程序和扩展程序同时尝试为同一个API实体upsert Core Data条目会发生什么?为了看到这可能是如何发生的,请看一下upsert的事件序列:

Core Data Upsert Sequence:

  1. API解析代码解析给定API实体的uniqueID。
  2. 解析器执行Core Data获取,以匹配谓词中uniqueID等于解析的uniqueID的任何条目。
  3. 如果找不到现有条目,则解析器会为此API实体插入一个新的Core Data条目,并将其uniqueID属性设置为解析的uniqueID。
  4. 解析器保存托管对象上下文,将新条目数据推送到SQLite后备存储。

详细问题

假设主机应用程序和扩展名独立地同时解析相同API实体的API响应。 如果在它们完成步骤4之前,主机应用程序和扩展名都在步骤3达到,则它们都会尝试为相同的uniqueID插入新的Core Data条目。 当它们到达步骤4并在各自的托管对象上下文上调用save:时,Core Data将快乐地创建重复条目。

据我所知,Core Data没有任何标记属性为唯一的方法。我需要一个Core Data相当于SQLite INSERT OR IGNORE+UPDATE combo。否则,我需要一种“锁定”持久存储的SQLite后备存储的方法,这听起来像是麻烦的食谱。
是否有已知的方法来解决iOS 8扩展引入的这个相当新颖的问题?

当主机进入后台模式时,为什么不取消/暂停GET请求? - 3lvis
我在Core Data失效时也遇到了重复条目,因此在Core Data Upsert Sequence的第2步中,如果有多个结果,我会删除额外的插入操作。https://github.com/NSElvis/NSManagedObject-ANDYMapChanges/blob/master/Source/NSManagedObject%2BANDYMapChanges.m#L52 - 3lvis
2个回答

10

对于iOS 8扩展引入的这个相当新颖的问题,是否有已知的解决方法?

是的,使用iCloud和Core Data时应用的方法是相同的:允许重复发生,然后进行清理。这两种情况都存在创建重复条目的风险,并且没有完全可靠的方法可以防止它们。由于您拥有一个uniqueID键,因此在这方面您处于良好状态。

如Dave DeLong所指出的那样,避免问题最好是直接避免。如果不可能,您需要额外工作来处理它。

查找重复项将类似于:

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节中。

10

看起来最简单的方法是一开始就避免多个写入器。为什么不完全依靠缓存数据驱动您的扩展,然后仅从主要iOS应用程序更新数据存储?


这是推荐的方法。 - Dhaval Panchal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接