轨道:如何获取至少有一个孩子的对象



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

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

我最接近的是

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

(基于另一个答案(,但它确实效率低下,因为它为每个Parent运行单独的查询。

Parent.joins(:children).uniq.all

从 Rails 5.1 开始,uniq已被弃用,应该改用distinct

Parent.joins(:children).distinct

这是克里斯·贝利(Chris Bailey(回答的后续。 .all也从原始答案中删除,因为它没有添加任何内容。

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

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

EXIST比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

我刚刚根据您的需要修改了此解决方案。

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

您只需要一个具有不同限定符的内部联接

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 })

这是SUX出于多种原因的解决方案。我推荐厄尼·米勒斯挤压库,这将允许你做

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

尝试包括带有#includes()的孩子

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

这将进行 2 个查询:

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

[更新]

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

为此,您需要向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?使用计数器缓存。

相关内容

  • 没有找到相关文章

最新更新