核心数据:使用关系计数谓词

时间:2015-11-02 18:50:52

标签: core-data

我从Core Data中提取了几千个对象,我只想返回至少有一个与之相关的对象。

当我使用类似于以下内容的谓词时,获取对象需要很长时间。大约5-8秒:

NSPredicate(format: "relationName.@count > 0")

是否有更有效的方法来执行此提取,或者我应该将值缓存在对象中以进行快速查找(即hasRelatedObjects属性)。

如果缓存是最好的路线,我不会相信它是微不足道的。例如,如果我修改我的Tag对象,在willSave我可以获取关系计数并将其存储在我的新属性中。但是,如果相关对象在其关系的一侧将标记添加到自身,则Tag对象永远不会更改,因此willSave不会被调用。

如何确保是否调用myTag.addRelatedObject(obj)myTag对象已更新)或myObj.addRelatedTag(myTag)myObj已更新),该值是否已缓存?< / p>

2 个答案:

答案 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也应该被召唤。你确认它不是吗?