按对象 ID 对字典 NSFetchRequest 进行分组

Posted

技术标签:

【中文标题】按对象 ID 对字典 NSFetchRequest 进行分组【英文标题】:Grouping a dictionary NSFetchRequest by object ID 【发布时间】:2015-11-12 12:47:58 【问题描述】:

我需要返回一个对象列表及其相关对象的计数。似乎不可能在单个字典提取请求中执行此操作,因为我无法按 objectID 对提取结果进行分组。

let objectIDExpression = NSExpressionDescription()
objectIDExpression.name = "objectID"
objectIDExpression.expression = NSExpression.expressionForEvaluatedObject()
objectIDExpression.expressionResultType = NSAttributeType.ObjectIDAttributeType

let countExpression = NSExpressionDescription()
countExpression.name = "count"
countExpression.expression = NSExpression(forFunction: "count:", arguments: [NSExpression(forKeyPath: "entries")])
countExpression.expressionResultType = .Integer32AttributeType

let fetchRequest = NSFetchRequest(entityName: "Tag")
fetchRequest.resultType = .DictionaryResultType
fetchRequest.propertiesToFetch = [objectIDExpression, countExpression]
fetchRequest.propertiesToGroupBy = [objectIDExpression]

var error: NSError?
if let results = self.context.executeFetchRequest(fetchRequest, error: &error) 
    println(results)

当此请求执行时,会出现以下错误:

'Invalid keypath expression ((<NSExpressionDescription: 0x7f843bf2d470>), name objectID, isOptional 1, isTransient 0, entity (null), renamingIdentifier objectID, validation predicates (
), warnings (
), versionHashModifier (null)
 userInfo 
) passed to setPropertiesToFetch:'

我还测试了只是传递 "objectID" 表达式名称,但这也失败了。

因此没有办法按对象 ID 分组吗?

【问题讨论】:

貌似错误在[NSExpression(forKeyPath: "Entries")],大概是指一对多的关系,根据Core Data规则,名字不能大写。 抱歉,这是示例代码中的错字。我还详细说明了错误消息。需要明确的是,仅当我设置 propertiesToGroupBy 属性时才会发生错误。没有它,提取工作,但每个“标签”对象没有分组。 关系是一对多还是多对多?即一个Entry可以有多个Tag吗? @pbasdf 标签和条目之间是多对多的关系。 可惜...如果是一对多,您可以获取 Entries 并按 Tag 分组。我想您可以显式地建模与中间实体的多对多关系,并在获取中使用该实体。但这是一个混乱的解决方案,需要对代码的其余部分进行大规模更改。 【参考方案1】:

您可以在不使用 propertiesToGroupBy 的情况下获得所需的计数。 CoreData 似乎推断出正确的计数范围并使用 sub-SELECT 代替(奇怪的是,仅当 fetch 包含属性以及 objectID 和计数时,请参见下文)。例如,我有Tagmany-many 和Items

第一次尝试

我可以获取tagName 和项目数如下:

NSFetchRequest *fetch = [NSFetchRequest fetchRequestWithEntityName:@"Tag"];
NSExpressionDescription *countED = [NSExpressionDescription new];
countED.expression = [NSExpression expressionWithFormat:@"count:(items)"];
countED.name = @"countOfItems";
countED.expressionResultType = NSInteger64AttributeType;
fetch.resultType = NSDictionaryResultType;
fetch.propertiesToFetch = @[@"tagName", countED];
NSArray *results = [self.context executeFetchRequest:fetch error:nil];
NSLog(@"results is %@", results);

生成以下 SQL:

SELECT t0.ZTAGNAME, (SELECT COUNT(t1.Z_3ITEMS) FROM Z_3TAGS t1 WHERE (t0.Z_PK = t1.Z_8TAGS) ) FROM ZTAG t0

第二次尝试

遗憾的是,如果我尝试选择 objectID 而不是 tagName,CoreData 似乎会感到困惑:

NSExpressionDescription *selfED = [NSExpressionDescription new];
selfED.expression = [NSExpression expressionForEvaluatedObject];
selfED.name = @"self";
selfED.expressionResultType = NSObjectIDAttributeType;
fetch.resultType = NSDictionaryResultType;
fetch.propertiesToFetch = @[selfED, countED];

生成此 SQL:

SELECT t0.Z_ENT, t0.Z_PK, COUNT( t1.Z_3ITEMS) FROM ZTAG t0 LEFT OUTER JOIN Z_3TAGS t1 ON t0.Z_PK = t1.Z_8TAGS

计算外连接中的所有行(并建议您需要按 objectID 分组,尽管我们知道这行不通)。

最后的尝试

但是,包括tagName objectID,一切都很好:

fetch.propertiesToFetch = @[selfED, @"tagName", countED];

给予:

SELECT t0.Z_ENT, t0.Z_PK, t0.ZTAGNAME, (SELECT COUNT(t1.Z_3ITEMS) FROM Z_3TAGS t1 WHERE (t0.Z_PK = t1.Z_8TAGS) ) FROM ZTAG t0

这似乎可以解决问题。 (很抱歉恢复到 Objective-C,并使用不同的实体/属性名称,但我相信你明白了)。

一边

我发现的另一个好奇心是,上面的第二次尝试也可以通过计算关系的属性而不是关系本身来实现:

countED.expression = [NSExpression expressionWithFormat:@"count:(items.itemName)"];
fetch.propertiesToFetch = @[selfED, countED];

给予:

SELECT t0.Z_ENT, t0.Z_PK, (SELECT COUNT(t2.ZITEMNAME) FROM Z_3TAGS t1 JOIN ZITEMS t2 ON t1.Z_3ITEMS = t2.Z_PK WHERE (t0.Z_PK = t1.Z_8TAGS) ) FROM ZTAG t0

如果itemName 不为零,这将(我认为)给出正确的计数。

【讨论】:

【参考方案2】:

我玩了一会儿,肯定有一些方法可以告诉核心数据按主键分组。

我想不通,但我相信这是可能的。

我能做的最好的就是添加另一个独特的属性“uuid”(无论如何,出于各种原因,我将它用于我的所有实体)。您可以使用NSUUID 轻松完成此操作,或者您可以采用永久对象 ID URI 表示并将其转换为字符串。

无论如何,我认为这可以满足您的需求,但这样做需要一个单独的唯一属性。

fetchRequest.propertiesToGroupBy = @[@"uuid"];

我尝试了一堆替代方案作为 group-by 属性,但 expressionForEvaluatedObject 总是失败,其他尝试都失败了。

我相信你已经知道了。以防万一,尽管它至少是一种解决方法,即使您不将它用于其他任何事情,直到有人出现之前实际上已经这样做过。

FWIW,这里是 SQL...

CoreData: sql: SELECT t0.Z_ENT, t0.Z_PK, COUNT( t1.Z_1ENTRIES), t0.ZUUID
    FROM ZTAG t0 LEFT OUTER JOIN Z_1TAGS t1 ON t0.Z_PK = t1.Z_2TAGS
    GROUP BY  t0.ZUUID

当然,必须有一种方法告诉它在 group-by 子句中替换 t0.Z_PK。我想这应该是expressionForEvaluatedObject 或“objectID”或“self”或“self.objectID”的简单特例

祝你好运,如果你解决了请报告。我会很感兴趣的。

【讨论】:

是的,很奇怪不是吗,一定有办法的!同时,正如您所说,我正在使用另一个独特的字段,但如果我找到任何解决方法,我会确保报告! @JodyHagins 这似乎可以不使用propertiesToGroupBy - 请参阅我的单独答案。【参考方案3】:

使用NSFetchedResultsController 可能更容易。您可以将sectionNameKeyPath 设置为分组并使用生成的NSIndexPaths 来构建您的字典。

话虽如此,我认为按 objectID 分组没有任何意义,因为根据定义,每个 object ID 都是唯一的。因此,每组中都会有一个实例。这可能是设置propertiesToGroupBy 失败的原因。

所以,简短的回答:不。

例如

let fetchRequest = NSFetchRequest(entityName: "Tag")
var output = [(NSManagedObjectID, Int)]()
do 
   let results = try context.executeFetchRequest(request) as! [Tag]
   for tag in results  
       output.append((tag.objectID, tag.entries.count))
   
 catch 
// output contains tuples with objectID and count

如果entries是可选的,请使用tag.entries?.count ?? 0

【讨论】:

我不确定这对我的情况是否有帮助。我基本上需要一种有效的方法来最终将Tag 对象映射到它拥有多少相关Entry 对象的整数计数。我不想简单地获取我的所有标签对象并计算关系集的大小,因为我不想将所有相关对象都放入上下文中,因为每个标签可能有数百个。而且我相信(虽然我可能错了)计算集合的大小会使相关对象出错。 IE。 count(tag.entries) 查看我修改后的答案。我认为对象 ID 存在一些混淆。 每个 objectID 都是唯一的,所以我想按它进行分组。我想要一个字典数组,每个字典都有 2 个键、objectID 以及与之相关的条目数 我认为使用 FRC 会非常有效。我看不到条目将如何被默认。只需遍历fetchedObjects 并获取entries.count。我在第一台 iPad 等旧硬件上用 100.000 条记录做了类似的事情,没有任何问题。对所有Tags 的获取请求将以类似的方式执行。 似乎entries.count 会导致数据库命中以创建相关的对象故障。如果您设置relationshipKeyPathsForPrefetching,那么它将批量获取关系数据,因此对entries.count 的每次调用将不再命中数据库(如果有很多对象要迭代,那就完美了)。我认为常规获取(带预取)和计数关系集将是解决此问题的最佳方法,直到可以使用按对象对字典获取进行分组的方法。

以上是关于按对象 ID 对字典 NSFetchRequest 进行分组的主要内容,如果未能解决你的问题,请参考以下文章

iOS Core Data NSFetchRequest groupby 关系对象

核心数据 NSFetchRequest 按类别排序方法返回值

排除某些对象的一对多关系上的 NSFetchRequest 无法按预期工作

使用 NSFetchRequest 按日期和单元格按时区字母顺序对部分进行排序

按计算结果排序 NSFetchRequest

从 NSFetchRequest 将字典数组转换为值数组