我从Core Data中提取了几千个对象,我只想返回至少有一个与之相关的对象。
当我使用类似于以下内容的谓词时,获取对象需要很长时间。大约5-8秒:
NSPredicate(format: "relationName.@count > 0")
是否有更有效的方法来执行此提取,或者我应该将值缓存在对象中以进行快速查找(即hasRelatedObjects
属性)。
如果缓存是最好的路线,我不会相信它是微不足道的。例如,如果我修改我的Tag
对象,在willSave
我可以获取关系计数并将其存储在我的新属性中。但是,如果相关对象在其关系的一侧将标记添加到自身,则Tag
对象永远不会更改,因此willSave
不会被调用。
如何确保是否调用myTag.addRelatedObject(obj)
(myTag
对象已更新)或myObj.addRelatedTag(myTag)
(myObj
已更新),该值是否已缓存?< / p>
答案 0 :(得分:3)
首先,让我们进行一些原型设计,看看这个fetch正在做什么。我假设您使用的是SQLite商店。
我攻击了一个类似于你描述的快速模型。
我定义了一个与Subentity有多对多关系的实体,其中Subentity具有一对一的反比关系。
现在,我在模拟器中进行测试,因此我创建了一个包含10mm实体的数据库。每次创建新实体时,它至少有2%的可能性为其创建至少一个子实体。如此选择的每个实体随机得到1到10个子实体。
因此,我最终获得了一个包含10,000,000个Entity对象和1,101,223个Subentity对象的数据库,其中199,788个Entity对象在其关系中至少有一个Subentity。
对于最简单的获取请求(与示例中的相同),我们得到此代码...
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Entity"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"subentities.@count != 0"];
NSArray *results = [moc executeFetchRequest:fetchRequest error:NULL];
和生成的SQL,以及执行提取所花费的时间。
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 WHERE (SELECT COUNT(t1.Z_PK) FROM ZSUBENTITY t1
WHERE (t0.Z_PK = t1.ZENTITY) ) <> ?
CoreData: annotation: sql connection fetch time: 17.9598s
CoreData: annotation: total fetch execution time: 17.9657s for 199788 rows.
如果您对SQL了解很多,可以看到查询不是最优的。两张桌子下面都有太多的东西。
如果我们只是为关系数量添加一个缓存,我们会得到这个结果(注意表格没有计入索引)。
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Entity"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"subcount != 0"];
NSArray *results = [moc executeFetchRequest:fetchRequest error:NULL];
然后我们得到这些结果......
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 WHERE t0.ZSUBCOUNT <> ?
CoreData: annotation: sql connection fetch time: 1.5795s
CoreData: annotation: total fetch execution time: 1.5838s for 199788 rows.
现在,让我们看看如果我们索引subcount
字段会发生什么。
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 WHERE t0.ZSUBCOUNT <> ?
CoreData: annotation: sql connection fetch time: 1.5749s
CoreData: annotation: total fetch execution time: 1.5788s for 199788 rows.
嗯。好多了。如果我们稍微改变谓词怎么办...
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 WHERE t0.ZSUBCOUNT > ?
CoreData: annotation: sql connection fetch time: 0.7805s
CoreData: annotation: total fetch execution time: 0.7843s for 199788 rows.
现在,花了一半的时间。我不确定原因,因为即使较慢的路径进行了两次二进制搜索,也没有值小于0的记录。
而且,我希望有一个更好的改进,基于以下事实:对于排序索引,它应该能够进行二分搜索,这应该比完整线性扫描的速度快一半。
无论如何,它确实表明它可以比这更快。
为了看看我们的下限是什么,我们可以做到这一点......
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Test"];
fetchRequest.fetchLimit = 199788;
NSArray *results = [moc executeFetchRequest:fetchRequest error:NULL];
给出了这些结果,以及我们可以期望抓住那么多记录的最佳结果,因为它基本上没有搜索。
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 LIMIT 199788
CoreData: annotation: sql connection fetch time: 0.1284s
CoreData: annotation: total fetch execution time: 0.1364s for 199788 rows.
现在,如果我们只关心它们是否为空,并且我们不关心实际计数,我们可以将缓存计数改为布尔值,它总是0或1。 / p>
采用这种方法,我们使用谓词
进行提取fetchRequest.predicate = [NSPredicate predicateWithFormat:@"subcount > 0"];
产量
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 WHERE t0.ZSUBCOUNT > ?
CoreData: annotation: sql connection fetch time: 0.5312s
CoreData: annotation: total fetch execution time: 0.5351s for 199788 rows.
将谓词更改回此
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"subcount != 0"];
产量
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 WHERE t0.ZSUBCOUNT <> ?
CoreData: annotation: sql connection fetch time: 1.5619s
CoreData: annotation: total fetch execution time: 1.5657s for 199788 rows.
这一个
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"subcount == 1"];
产量
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSUBCOUNT
FROM ZENTITY t0 WHERE t0.ZSUBCOUNT = ?
CoreData: annotation: sql connection fetch time: 0.5332s
CoreData: annotation: total fetch execution time: 0.5366s for 199788 rows.
所以,骨头上还有一些肉,但我会让你有一些一些的乐趣。
好的,所以我们想要缓存更改,我们如何才能实现这一目标?
嗯,最简单的方法是提供一个自定义方法,每次关系更改时都会使用该方法。但是,它要求所有更改都经过这一切,而且某些代码总是有可能在特殊API之外操作对象。
好吧,注意到计算值需要更新的一种方法是在对象保存时。您可以覆盖willSave
并在那里进行必要的更改。您还可以观察上下文保存通知并在那里进行工作。
对我而言,这种方法的主要问题是&#34;将节省&#34;通知在验证和与持久性存储合并之前发生。这些过程中的任何一个都可以改变数据,并且存在一些可能导致问题的棘手的合并问题。
真正确保验证和合并已成为核心的唯一方法是进入验证阶段。
不幸的是,Apple文档强烈反对这种方法。不过,我在这种模式上取得了很大的成功。
- (BOOL)validateSubcount:(id*)ioValue error:(NSError**)outError
{
NSUInteger computedValue = [*ioValue unsignedIntegerValue];
NSUInteger actualValue = computedValue;
NSString *key = @"subentities";
if ([self hasFaultForRelationshipNamed:key]) {
if (self.changedValues[@"subcount"]) {
if (has_objectIDsForRelationshipNamed) {
actualValue = [[self objectIDsForRelationshipNamed:key] count];
} else {
actualValue = [[self valueForKey:key] count];
}
}
} else {
actualValue = [[self valueForKey:key] count];
}
if (computedValue != actualValue) {
*ioValue = @(actualValue);
}
return YES;
}
这在保存时会自动调用,如果您想要更多&#34;频繁&#,您可以手动调用它(通过validateValue:forKey:error :)从对象 - 更改通知(或任何其他地方) 34;保存时的一致性。
关于改变一对一关系的问题;核心数据将正确处理反向关系。此外,所涉及的所有对象都会反映出适当的变化。
具体而言,如果您更改了子实体的一对一关系。您现在将拥有三个更新的对象:子实体本身,以前位于关系另一端的实体,以及现在位于关系另一端的实体。
答案 1 :(得分:1)
你当然已经定义了反向关系,对吧?所以你的关系中的didSet
处理程序也应该被调用,即使它从另一方面改变了。
的确,我认为willSave
也应该被召唤。你确认它不是吗?