【问题标题】:Rails: How to get objects with at least one child?Rails:如何获得至少有一个孩子的对象?
【发布时间】:2012-04-12 22:47:36
【问题描述】:

在谷歌搜索、浏览 SO 和 reading 之后,似乎没有一种 Rails 风格的方法可以有效地只获取那些 Parent 具有至少一个 @的对象987654324@ 对象(通过has_many :children 关系)。在纯 SQL 中:

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

离我最近的是

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

(基于another answer),但它确实效率低下,因为它为每个Parent运行单独的查询。

【问题讨论】:

    标签: ruby-on-rails activerecord


    【解决方案1】:

    Rails 5.1 开始,uniq 已被弃用,而应使用distinct

    Parent.joins(:children).distinct
    

    这是Chris Bailey's answer 的后续活动。 .all 也从原始答案中删除,因为它没有添加任何内容。

    【讨论】:

      【解决方案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 快得多。例如,这是一个有 cmets 和 likes 的帖子模型:

      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 个 cmets 和 50 个赞。只有一个帖子没有cmet和点赞:

      # 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 个 cmets * 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

      【讨论】:

      【解决方案3】:
      Parent.joins(:children).uniq.all
      

      【讨论】:

      • 这会产生一条 SQL 语句,并且简短易读。太棒了。
      • 是的,你做到了。 Parent.joins(:children).uniq.all 是一个数组,Parent.joins(:children).uniq 是一个 ActiveRelation 对象。注意 ActiveRelation 对象是惰性的,在明确请求之前不会执行。调用 all 会强制对象使用 DB 评估 SQL
      • 为什么会这样?我了解 SQL,但是...有人可以解释一下吗?
      • 巫术!如何为多个 has_many 关联做到这一点
      • 纯SQL版本:Parent.joins(:children).distinct.all
      【解决方案4】:

      尝试使用#includes() 包含孩子

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

      这将进行 2 个查询:

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

      [更新]

      当您需要加载子对象时,上述解决方案很有用。 但是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? 使用。

      【讨论】:

      • 这是一个错误的答案,因为正确的答案是内连接。以上是非常低效的,并且会进行多次查询和 ruby​​ 循环。
      • @bradgonesurfing 不,我的第一个解决方案永远不会在遍历父母时进行多次查询。 (注意.includes(:children))虽然Rails 可能会在需要时将上面的2 个查询变成1 个查询(使用JOIN),但这是真的。
      • 从未说过它会在循环时进行多个查询。您的解决方案确实进行了“多个查询”,其中两个,然后您使用拒绝循环遍历返回的 ruby​​ 集合。与数据库中的快速内部连接相比,这非常慢。
      • 但是公平地说,一般来说,您提出的解决方案确实解决了 OP 在其原始解决方案中看到的一般 1+N 问题。对于这个特定问题,这不是正确的解决方案:)
      【解决方案5】:

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

      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}
      

      【讨论】:

      • 当您出于某种原因需要使用JOIN 时,请查看@chris-bailey 的答案,同时以简洁明了的方式编写它。
      • 这相当于@ChrisBailey 的回答——不行。
      • 这与我提出的解决方案相同。然而,只需要加入的情况很少见。 Squeel 非常适合 AR 难以处理的复杂查询。
      【解决方案6】:

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

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

      【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-11-25
      • 2011-02-04
      • 1970-01-01
      • 1970-01-01
      • 2017-05-25
      • 1970-01-01
      相关资源
      最近更新 更多