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

时间:2012-03-29 09:42:41

标签: ruby-on-rails activerecord

在谷歌搜索,浏览SO和reading后,似乎没有一种Rails风格的方式来有效地只获得至少一个 <{1}}个对象/ em> Parent对象(通过Child关系)。在纯SQL中:

has_many :children

我最接近的是

SELECT *
  FROM parents
 WHERE EXISTS (
               SELECT 1
                 FROM children
                WHERE parent_id = parents.id)

(基于another answer),但效率非常低,因为它为每个Parent.all.reject { |parent| parent.children.empty? } 运行单独的查询。

6 个答案:

答案 0 :(得分:52)

Parent.joins(:children).uniq.all

答案 1 :(得分:3)

我刚刚根据您的需要修改了此solution

Parent.joins("left join childrens on childrends.parent_id = parents.id").where("childrents.parent_id is not null")

答案 2 :(得分:2)

尝试将孩子包括#includes()

Parent.includes(:children).all.reject { |parent| parent.children.empty? }

这将产生2个查询:

SELECT * FROM parents;
SELECT * FROM children WHERE parent_id IN (5, 6, 8, ...);

<强> [UPDATE]

当您需要加载Child对象时,上述解决方案很有用。 但children.empty?也可以使用计数器缓存 12 来确定子女数量。

为此,您需要在parents表中添加一个新列:

# a new migration
def up
  change_table :parents do |t|
    t.integer :children_count, :default => 0
  end

  Parent.reset_column_information
  Parent.all.each do |p|
    Parent.update_counters p.id, :children_count => p.children.length
  end
end

def down
  change_table :parents do |t|
    t.remove :children_count
  end
end

现在更改您的Child型号:

class Child
  belongs_to :parent, :counter_cache => true
end

此时您可以使用sizeempty?而无需触及children表:

Parent.all.reject { |parent| parent.children.empty? }

请注意,length不使用计数器缓存,而sizeempty?则使用。

答案 3 :(得分:2)

你只想要一个带有不同限定符的内连接

SELECT DISTINCT(*) 
FROM parents
JOIN children
ON children.parent_id = parents.id

这可以在标准活动记录中完成

Parent.joins(:children).uniq

但是,如果你想找到更复杂的结果,找不到所有没有孩子的父母 你需要一个外部联接

Parent.joins("LEFT OUTER JOIN children on children.parent_id = parent.id").
where(:children => { :id => nil })

这是一个解决方案,因为很多原因。我推荐Ernie Millers squeel库让你做

Parent.joins{children.outer}.where{children.id == nil}

答案 4 :(得分:2)

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

Parent.where<<-SQL
EXISTS (SELECT * FROM children c WHERE c.parent_id = parents.id)
SQL

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

在数据库中有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')

如果您想获得至少有一条评论等的帖子,您可以这样写:

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

上面的查询生成如下SQL:

SELECT DISTINCT "posts".* 
FROM "posts" 
INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" 
INNER JOIN "likes" ON "likes"."post_id" = "posts"."id"

但是这个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

此查询生成如下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)
)

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

这是基准:

              user     system      total        real
Uniq:     0.010000   0.000000   0.010000 (  0.074396)
Exists:   0.000000   0.000000   0.000000 (  0.003711)

它显示EXISTS比DISTINCT快20.047661倍。

我在GitHub中推送了示例应用程序,因此您可以自己确认差异:

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

答案 5 :(得分:2)

Rails 5.1起,不推荐使用uniq,而应使用distinct

Parent.joins(:children).distinct

这是Chris Bailey's answer的后续行动。 <{1}}也会从原始答案中移除,因为它不会添加任何内容。