如何在Rails中创建作用域,分组获取每个Topic的最高edition记录?
嘿,我懂你的痛点——你用TopicEdition.group(:topic_id).maximum(:edition)只能得到一个键为topic_id、值为对应最大edition的哈希,但你需要的是完整的TopicEdition记录(就像你例子里的id=2和3那两条),对吧?别着急,这里有几种实用的方法可以实现:
方法一:子查询关联法(通用所有数据库)
这是最通用的方案,不管你用MySQL还是PostgreSQL都能跑通。思路是先通过子查询算出每个topic的最大edition,再把原表和这个子查询结果做关联,匹配出对应的完整记录:
# 先构建子查询:得到每个topic_id对应的最大edition max_edition_subquery = TopicEdition.group(:topic_id).select("topic_id, MAX(edition) AS max_edition") # 关联原表,筛选出符合条件的记录 TopicEdition.joins( "INNER JOIN (#{max_edition_subquery.to_sql}) max_eds ON topic_editions.topic_id = max_eds.topic_id AND topic_editions.edition = max_eds.max_edition" )
这个查询会直接返回你需要的完整TopicEdition对象集合,完美匹配你的需求。
方法二:窗口函数法(Rails 5+ 支持)
如果你的Rails版本在5及以上,可以用SQL窗口函数来实现,代码更简洁,逻辑也清晰:
TopicEdition.select("topic_editions.*").from( # 给每个topic下的记录按edition降序排名 TopicEdition.select("*, RANK() OVER (PARTITION BY topic_id ORDER BY edition DESC) AS rank"), :topic_editions ).where("rank = 1")
这里的RANK()函数会给同一个topic_id下的记录按edition从高到低排名,我们只取排名为1的记录就行。如果同一个topic有多个相同最高edition的记录(比如两条edition都是2的),这个方法会返回所有符合的记录;如果你只想取其中一条,可以把RANK()换成ROW_NUMBER()。
方法三:distinct_on 法(仅PostgreSQL支持,Rails 7+ 更友好)
如果你用的是PostgreSQL数据库,那这个方法是最简洁的:
TopicEdition.order(topic_id: :asc, edition: :desc).distinct_on(:topic_id)
原理很简单:distinct_on(:topic_id)会保留每个topic_id对应的第一条记录,我们先按topic_id排序,再按edition降序排序,这样每个topic的第一条记录就是edition最高的那条,一行代码搞定!
为什么你之前的方法不行?
你之前用的group(:topic_id).maximum(:edition)本质是做了聚合查询,SQL的分组规则决定了:分组后你只能选择分组字段(这里是topic_id)和聚合函数的结果(这里是MAX(edition)),没办法直接拿到对应记录的id或其他字段——因为分组后数据库不知道要返回同组里的哪一条记录的其他字段,所以必须用上面这些方法来关联或者做窗口筛选。
内容的提问来源于stack exchange,提问作者Sig

