Rails:如何获得至少一个孩子的对象?

l0b*_*0b0 20 activerecord ruby-on-rails

在谷歌搜索,浏览SO和阅读之后,似乎没有一种Rails风格的方式来有效地仅获得Parent具有至少一个 Child对象(通过has_many :children关系)的那些对象.在纯SQL中:

SELECT *
  FROM parents
 WHERE EXISTS (
               SELECT 1
                 FROM children
                WHERE parent_id = parents.id)
Run Code Online (Sandbox Code Playgroud)

我最接近的是

Parent.all.reject { |parent| parent.children.empty? }
Run Code Online (Sandbox Code Playgroud)

(基于另一个答案),但它的效率非常低,因为它为每个答案运行一个单独的查询Parent.

Chr*_*ley 53

Parent.joins(:children).uniq.all
Run Code Online (Sandbox Code Playgroud)

  • 为什么这样做?我理解SQL但是...有人可以解释一下吗? (4认同)
  • 是的,你做到了.**Parent.joins(:children).uniq.all**是一个数组,**Parent.joins(:children).uniq**是一个ActiveRelation对象.注意ActiveRelation对象是惰性的,在请求显式之前不会执行.调用**all**会强制对象使用DB来评估SQL (3认同)

Fel*_*ger 9

Rails 5.1 开始uniq已弃用,distinct应改为使用。

Parent.joins(:children).distinct
Run Code Online (Sandbox Code Playgroud)

这是对Chris Bailey 的回答的跟进。.all也从原始答案中删除,因为它没有添加任何内容。


Jun*_*Ito 6

接受的答案 ( Parent.joins(:children).uniq) 使用 DISTINCT 生成 SQL,但查询速度可能很慢。为了获得更好的性能,您应该使用 EXISTS 编写 SQL:

Parent.where<<-SQL
EXISTS (SELECT * FROM children c WHERE c.parent_id = parents.id)
SQL
Run Code Online (Sandbox Code Playgroud)

EXISTS 比 DISTINCT 快得多。例如,这是一个包含评论和点赞的帖子模型:

class Post < ApplicationRecord
  has_many :comments
  has_many :likes
end

class Comment < ApplicationRecord
  belongs_to :post
end

class Like < ApplicationRecord
  belongs_to :post
end
Run Code Online (Sandbox Code Playgroud)

数据库中有 100 个帖子,每个帖子有 50 条评论和 50 条点赞。只有一篇帖子没有评论和点赞:

# Create posts with comments and likes
100.times do |i|
  post = Post.create!(title: "Post #{i}")
  50.times do |j|
    post.comments.create!(content: "Comment #{j} for #{post.title}")
    post.likes.create!(user_name: "User #{j} for #{post.title}")
  end
end

# Create a post without comment and like
Post.create!(title: 'Hidden post')
Run Code Online (Sandbox Code Playgroud)

如果你想获得至少有一条评论和点赞的帖子,你可以这样写:

# NOTE: uniq method will be removed in Rails 5.1
Post.joins(:comments, :likes).distinct
Run Code Online (Sandbox Code Playgroud)

上面的查询生成如下 SQL:

SELECT DISTINCT "posts".* 
FROM "posts" 
INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" 
INNER JOIN "likes" ON "likes"."post_id" = "posts"."id"
Run Code Online (Sandbox Code Playgroud)

但是这个 SQL 生成 250000 行(100 个帖子 * 50 个评论 * 50 个点赞),然后过滤掉重复的行,因此可能会很慢。

在这种情况下你应该这样写:

Post.where <<-SQL
EXISTS (SELECT * FROM comments c WHERE c.post_id = posts.id)
AND
EXISTS (SELECT * FROM likes l WHERE l.post_id = posts.id)
SQL
Run Code Online (Sandbox Code Playgroud)

该查询生成如下 SQL:

SELECT "posts".* 
FROM "posts" 
WHERE (
EXISTS (SELECT * FROM comments c WHERE c.post_id = posts.id) 
AND 
EXISTS (SELECT * FROM likes l WHERE l.post_id = posts.id)
)
Run Code Online (Sandbox Code Playgroud)

此查询不会生成无用的重复行,因此速度可能会更快。

这是基准:

              user     system      total        real
Uniq:     0.010000   0.000000   0.010000 (  0.074396)
Exists:   0.000000   0.000000   0.000000 (  0.003711)
Run Code Online (Sandbox Code Playgroud)

它表明 EXISTS 比 DISTINCT 快 20.047661 倍。

我在GitHub上推送了示例应用程序,所以你可以自己确认差异:

https://github.com/JunichiIto/exists-query-sandbox