我从Core数据中获取了几千个对象,我只想返回那些至少有一个与其相关的对象。
当我使用类似于下面的谓词时,获取对象需要很长的时间。大约5-8秒:
NSPredicate(format: "relationName.@count > 0")
是否有一种更有效的方法来执行此提取,或者应该在对象中缓存该值以进行快速查找(即hasRelatedObjects
属性)。
如果缓存是最好的路径,我不认为它是微不足道的。例如,如果我修改我的Tag
对象,我可以在willSave
中获取关系计数,并将其存储在我的新属性中。但是,如果相关对象将标记添加到关系的自身,则Tag
对象永远不会更改,因此不会调用willSave
。
如何确保您调用myTag.addRelatedObject(obj)
(myTag
对象被更新)或myObj.addRelatedTag(myTag)
(myObj
被更新),该值被缓存?
发布于 2015-11-03 09:49:21
首先,让我们来做一些原型化,看看这个fetch正在做什么。我想您使用的是SQLite商店。
我黑了一个快速模型,跟你描述的一样。
我定义了一个实体,它与子实体有一对多的关系,其中子实体有一对一的逆关系。
现在,我在模拟器中进行测试,所以我创建了一个包含10 so实体的数据库。每次创建新实体时,至少为其创建一个子实体的可能性约为2%。每个被随机选择的实体在1到10个子实体之间。
因此,我得到了一个数据库,数据库中有10,000,000个实体对象,以及1,101,223个子实体对象,其中199,788个实体对象的关系中至少有一个子实体。
对于最简单的提取请求(与示例中的请求相同),我们得到以下代码.
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。
通过采用这种方法,我们的fetch with谓词
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
并在那里进行任何必要的更改。您还可以观察到上下文-将保存通知,并在其中执行工作。
对我来说,这种方法的主要问题是“将保存”通知发生在验证和与持久存储合并之前。这两个过程中的任何一个都可能更改数据,并且有一些棘手的合并问题可能会导致问题。
真正确保验证和合并已经成为核心的唯一方法是连接到验证阶段。
不幸的是,苹果文档强烈反对这种方法。不过,我在这个模式上取得了很好的成功。
- (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;
}
这将在保存时自动调用,并且您可以手动调用它(通过validateValue:forKey:error:),如果您想要更多的“频繁”一致性,而不仅仅是在保存时,您可以从对象-确实更改通知(或其他任何地方)调用它。
关于更改为一关系的问题;核心数据将正确处理逆关系。此外,所有涉及的对象都将得到适当的更改。
具体来说,如果你改变了一个子实体的关系。现在您将有三个更新的对象:子实体本身、过去位于关系另一端的实体和现在处于关系另一端的实体。
发布于 2015-11-02 22:20:18
你当然定义了反向关系,对吗?因此,您的关系上的didSet
处理程序也应该被调用,即使它是从另一方更改的。
事实上,我认为willSave
也应该被称为。你核实过不是吗?
https://stackoverflow.com/questions/33484766
复制相似问题