【问题标题】:LEFT OUTER JOIN in Rails 4Rails 4中的左外连接
【发布时间】:2014-08-13 01:45:13
【问题描述】:

我有 3 个模型:

class Student < ActiveRecord::Base
  has_many :student_enrollments, dependent: :destroy
  has_many :courses, through: :student_enrollments
end

class Course < ActiveRecord::Base   
    has_many :student_enrollments, dependent: :destroy
    has_many :students, through: :student_enrollments
end

class StudentEnrollment < ActiveRecord::Base
    belongs_to :student
    belongs_to :course
end

我希望在 Courses 表中查询与某个学生相关联的 StudentEnrollments 表中不存在的课程列表。

我发现也许 Left Join 是要走的路,但似乎 rails 中的 joins() 只接受一个表作为参数。 我认为会做我想做的 SQL 查询是:

SELECT *
FROM Courses c LEFT JOIN StudentEnrollment se ON c.id = se.course_id
WHERE se.id IS NULL AND se.student_id = <SOME_STUDENT_ID_VALUE> and c.active = true

如何以 Rails 4 方式执行此查询?

感谢任何输入。

【问题讨论】:

  • 如果StudentEnrollments中不存在该记录,那么se.student_id = &lt;SOME_STUDENT_ID_VALUE&gt;肯定是不可能的吗?

标签: sql ruby-on-rails ruby-on-rails-4 rails-activerecord mysql2


【解决方案1】:

您也可以传递一个作为 join-sql 的字符串。例如joins("LEFT JOIN StudentEnrollment se ON c.id = se.course_id")

虽然为了清晰起见,我会使用 rails 标准的表格命名:

joins("LEFT JOIN student_enrollments ON courses.id = student_enrollments.course_id")

【讨论】:

  • 我的解决方案最终是:query = "LEFT JOIN student_enrollments ON courses.id = student_enrollments.course_id AND" + " student_enrollments.student_id = #{self.id}" courses = Course.active.joins (query) .where(student_enrollments: {id: nil}) 虽然它完成了工作,但它不像我想要的那样是 Rails。我尝试使用 .includes(),它执行 LEFT JOIN,但它不允许我在加入时指定额外条件。谢谢塔林!
  • 太棒了。嘿,有时我们会做我们所做的事情来让它发挥作用。是时候回到它并在未来让它变得更好...... :)
  • @TarynEast “让它发挥作用,让它变得更快,让它变得美丽。” :)
【解决方案2】:

您将执行查询:

Course.joins('LEFT JOIN student_enrollment on courses.id = student_enrollment.course_id')
      .where(active: true, student_enrollments: { student_id: SOME_VALUE, id: nil })

【讨论】:

    【解决方案3】:

    这是 Rails 中 Active Model 中的连接查询。

    Please click here for More info about Active Model Query Format.

    @course= Course.joins("LEFT OUTER JOIN StudentEnrollment 
         ON StudentEnrollment .id = Courses.user_id").
         where("StudentEnrollment .id IS NULL AND StudentEnrollment .student_id = 
        <SOME_STUDENT_ID_VALUE> and Courses.active = true").select
    

    【讨论】:

    • 最好在您发布的答案中添加一些解释。
    【解决方案4】:

    实际上有一种“Rails 方式”可以做到这一点。

    您可以使用Arel,这是 Rails 用来构造 ActiveRecrods 查询的工具

    我会将它包装在方法中,以便您可以很好地调用它并传入您想要的任何参数,例如:

    class Course < ActiveRecord::Base
      ....
      def left_join_student_enrollments(some_user)
        courses = Course.arel_table
        student_entrollments = StudentEnrollment.arel_table
    
        enrollments = courses.join(student_enrollments, Arel::Nodes::OuterJoin).
                      on(courses[:id].eq(student_enrollments[:course_id])).
                      join_sources
    
        joins(enrollments).where(
          student_enrollments: {student_id: some_user.id, id: nil},
          active: true
        )
      end
      ....
    end
    

    还有很多人使用的快速(但有点脏)的方式

    Course.eager_load(:students).where(
        student_enrollments: {student_id: some_user.id, id: nil}, 
        active: true
    )
    

    eager_load 效果很好,它只是在内存中加载模型的“副作用”,你可能不需要(就像你的情况一样)
    请参阅 Rails ActiveRecord::QueryMethods .eager_load
    它以一种简洁的方式完全满足您的要求。

    【讨论】:

    • 我只能说我不敢相信 ActiveRecord 在这么多年后仍然没有内置支持。这是完全深不可测的。
    • Sooooo Sequel 什么时候可以成为 Rails 中的默认 ORM?
    • Rails 不应该变得臃肿。海事组织,当他们决定首先提取默认捆绑的宝石时,他们做对了。哲学是“少而精”和“选择你想要的”
    • Rails 5 支持左外连接:blog.bigbinary.com/2016/03/24/…
    • 为了避免 eager_load 的“副作用”,看我的回答
    【解决方案5】:

    我已经为这类问题苦苦挣扎了很长一段时间,并决定做点什么来一劳永逸地解决它。我发表了一篇 Gist 来解决这个问题:https://gist.github.com/nerde/b867cd87d580e97549f2

    我创建了一个小 AR hack,它使用 Arel Table 为您动态构建左连接,而无需在您的代码中编写原始 SQL:

    class ActiveRecord::Base
      # Does a left join through an association. Usage:
      #
      #     Book.left_join(:category)
      #     # SELECT "books".* FROM "books"
      #     # LEFT OUTER JOIN "categories"
      #     # ON "books"."category_id" = "categories"."id"
      #
      # It also works through association's associations, like `joins` does:
      #
      #     Book.left_join(category: :master_category)
      def self.left_join(*columns)
        _do_left_join columns.compact.flatten
      end
    
      private
    
      def self._do_left_join(column, this = self) # :nodoc:
        collection = self
        if column.is_a? Array
          column.each do |col|
            collection = collection._do_left_join(col, this)
          end
        elsif column.is_a? Hash
          column.each do |key, value|
            assoc = this.reflect_on_association(key)
            raise "#{this} has no association: #{key}." unless assoc
            collection = collection._left_join(assoc)
            collection = collection._do_left_join value, assoc.klass
          end
        else
          assoc = this.reflect_on_association(column)
          raise "#{this} has no association: #{column}." unless assoc
          collection = collection._left_join(assoc)
        end
        collection
      end
    
      def self._left_join(assoc) # :nodoc:
        source = assoc.active_record.arel_table
        pk = assoc.association_primary_key.to_sym
        joins source.join(assoc.klass.arel_table,
          Arel::Nodes::OuterJoin).on(source[assoc.foreign_key].eq(
            assoc.klass.arel_table[pk])).join_sources
      end
    end
    

    希望对你有帮助。

    【讨论】:

      【解决方案6】:

      使用Squeel:

      Person.joins{articles.inner}
      Person.joins{articles.outer}
      

      【讨论】:

      • Squeel 是一个不受支持的库,不推荐
      【解决方案7】:

      请参阅下面我对这个问题的原始帖子。

      从那时起,我为 ActiveRecord v4.0.x 实现了我自己的.left_joins()(抱歉,我的应用程序在这个版本中被冻结,所以我不需要将它移植到其他版本):

      在文件app/models/concerns/active_record_extensions.rb 中,输入以下内容:

      module ActiveRecordBaseExtensions
          extend ActiveSupport::Concern
      
          def left_joins(*args)
              self.class.left_joins(args)
          end
      
          module ClassMethods
              def left_joins(*args)
                  all.left_joins(args)
              end
          end
      end
      
      module ActiveRecordRelationExtensions
          extend ActiveSupport::Concern
      
          # a #left_joins implementation for Rails 4.0 (WARNING: this uses Rails 4.0 internals
          # and so probably only works for Rails 4.0; it'll probably need to be modified if
          # upgrading to a new Rails version, and will be obsolete in Rails 5 since it has its
          # own #left_joins implementation)
          def left_joins(*args)
              eager_load(args).construct_relation_for_association_calculations
          end
      end
      
      ActiveRecord::Base.send(:include, ActiveRecordBaseExtensions)
      ActiveRecord::Relation.send(:include, ActiveRecordRelationExtensions)
      

      现在我可以在我通常使用.joins() 的任何地方使用.left_joins()。

      -----------------下面是原帖-----------------

      如果您希望 OUTER JOIN 没有所有额外急切加载的 ActiveRecord 对象,请在 .eager_load() 之后使用 .pluck(:id) 中止急切加载,同时保留 OUTER JOIN。使用 .pluck(:id) 会阻止预加载,因为列名别名(例如 items.location AS t1_r9)在使用时会从生成的查询中消失(这些独立命名的字段用于实例化所有预加载的 ActiveRecord 对象)。

      这种方法的一个缺点是您需要运行第二个查询来提取在第一个查询中标识的所需 ActiveRecord 对象:

      # first query
      idents = Course
          .eager_load(:students)  # eager load for OUTER JOIN
          .where(
              student_enrollments: {student_id: some_user.id, id: nil}, 
              active: true
          )
          .distinct
          .pluck(:id)  # abort eager loading but preserve OUTER JOIN
      
      # second query
      Course.where(id: idents)
      

      【讨论】:

      • 这很有趣。
      • +1 但您可以再改进一点并使用select(:id) 而不是pluck(:id) 并防止实现内部查询,并将其全部留给数据库。
      【解决方案8】:

      结合 includes 和 where 会导致 ActiveRecord 在后台执行 LEFT OUTER JOIN(没有 where 这会生成正常的两个查询集)。

      所以你可以这样做:

      Course.includes(:student_enrollments).where(student_enrollments: { course_id: nil })
      

      这里的文档:http://guides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-eager-loaded-associations

      【讨论】:

        【解决方案9】:

        如果有人来这里寻找在 Rails 5 中进行左外连接的通用方法,您可以使用 #left_outer_joins 函数。

        多连接示例:

        鲁比:

        Source.
         select('sources.id', 'count(metrics.id)').
         left_outer_joins(:metrics).
         joins(:port).
         where('ports.auto_delete = ?', true).
         group('sources.id').
         having('count(metrics.id) = 0').
         all
        

        SQL:

        SELECT sources.id, count(metrics.id)
          FROM "sources"
          INNER JOIN "ports" ON "ports"."id" = "sources"."port_id"
          LEFT OUTER JOIN "metrics" ON "metrics"."source_id" = "sources"."id"
          WHERE (ports.auto_delete = 't')
          GROUP BY sources.id
          HAVING (count(metrics.id) = 0)
          ORDER BY "sources"."id" ASC
        

        【讨论】:

        • 谢谢,我想提一下交叉关联左外连接,使用left_outer_joins(a: [:b, :c])
        • 您也可以使用简写为left_joins 并且行为方式相同。例如left_joins(:order_reports)
        【解决方案10】:

        添加到上面的答案,使用includes,如果你想要一个OUTER JOIN而不引用where中的表(比如id是nil)或者引用在一个字符串中,你可以使用references。看起来像这样:

        Course.includes(:student_enrollments).references(:student_enrollments)
        

        或

        Course.includes(:student_enrollments).references(:student_enrollments).where('student_enrollments.id = ?', nil)
        

        http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-references

        【讨论】:

        • 这是否适用于深度嵌套的关系,或者该关系是否需要直接挂在被查询的模型上?我似乎找不到任何前者的例子。
        • 爱它!只需将joins 替换为includes 就可以了。
        【解决方案11】:

        您可以使用 left_joins gem,它将 Rails 5 中的 left_joins 方法反向移植到 Rails 4 和 3。

        Course.left_joins(:student_enrollments)
              .where('student_enrollments.id' => nil)
        

        【讨论】:

          【解决方案12】:

          我知道这是一个老问题和老线程,但在 Rails 5 中,你可以简单地这样做

          Course.left_outer_joins(:student_enrollments)
          

          【讨论】:

          • 这个问题专门针对 Rails 4.2。
          猜你喜欢
          • 1970-01-01
          • 2018-08-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-10-01
          • 2011-02-24
          • 1970-01-01
          相关资源
          最近更新 更多