简体   繁体   English

Rails 4中的左外连接

[英]LEFT OUTER JOIN in Rails 4

I have 3 models:我有 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

I wish to query for a list of courses in the Courses table, that do not exist in the StudentEnrollments table that are associated with a certain student.我希望在 Courses 表中查询与某个学生关联的 StudentEnrollments 表中不存在的课程列表。

I found that perhaps Left Join is the way to go, but it seems that joins() in rails only accept a table as argument.我发现也许 Left Join 是要走的路,但似乎 rails 中的 joins() 只接受一个表作为参数。 The SQL query that I think would do what I want is:我认为会做我想做的 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

How do I execute this query the Rails 4 way?如何以 Rails 4 方式执行此查询?

Any input is appreciated.任何输入表示赞赏。

You can pass a string that is the join-sql too.您也可以传递一个作为 join-sql 的字符串。 eg joins("LEFT JOIN StudentEnrollment se ON c.id = se.course_id")例如joins("LEFT JOIN StudentEnrollment se ON c.id = se.course_id")

Though I'd use rails-standard table naming for clarity:虽然为了清楚起见,我会使用 rails-standard 表命名:

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

If anyone came here looking for a generic way to do a left outer join in Rails 5, you can use the #left_outer_joins function.如果有人来这里寻找在 Rails 5 中进行左外连接的通用方法,您可以使用#left_outer_joins函数。

Multi-join example:多连接示例:

Ruby:红宝石:

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: 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

There is actually a "Rails Way" to do this.实际上有一个“Rails Way”可以做到这一点。

You could use Arel , which is what Rails uses to construct queries for ActiveRecrods您可以使用Arel ,这是 Rails 用来构造 ActiveRecrods 查询的工具

I would wrap it in method so that you can call it nicely and pass in whatever argument you would like, something like:我会将它包装在方法中,以便您可以很好地调用它并传入您想要的任何参数,例如:

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

There is also the quick (and slightly dirty) way that many use还有许多人使用的快速(而且有点脏)的方式

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

eager_load works great, it just has the "side effect" of loding models in memory that you might not need (like in your case) eager_load 效果很好,它只是在你可能不需要的内存中加载模型的“副作用”(就像你的情况一样)
Please see Rails ActiveRecord::QueryMethods .eager_load请参阅 Rails ActiveRecord::QueryMethods .eager_load
It does exactly what you are asking in a neat way.它以一种简洁的方式完全满足您的要求。

Combining includes and where results in ActiveRecord performing a LEFT OUTER JOIN behind the scenes (without the where this would generate the normal set of two queries).结合includeswhere会导致 ActiveRecord 在幕后执行 LEFT OUTER JOIN(没有 where 这将生成正常的两个查询集)。

So you could do something like:因此,您可以执行以下操作:

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

Docs here: http://guides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-eager-loaded-associations此处的文档:http: //guides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-eager-loaded-associations

Adding to the answer above, to use includes , if you want an OUTER JOIN without referencing the table in the where (like id being nil) or the reference is in a string you can use references .添加到上面的答案,使用includes ,如果你想要一个 OUTER JOIN 而不引用 where 中的表(如 id 为 nil)或引用在一个字符串中,你可以使用references That would look like this:看起来像这样:

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

or或者

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

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

You'd execute the query as:您将执行查询为:

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

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

Course.left_outer_joins(:student_enrollments)

You could use left_joins gem, which backports left_joins method from Rails 5 for Rails 4 and 3.您可以使用left_joins gem,它将 Rails 5 中的left_joins方法反向移植到 Rails 4 和 3。

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

I've been struggling with this kind of problem for quite some while, and decided to do something to solve it once and for all.我已经为这种问题苦苦挣扎了很长一段时间,并决定做一些事情来一劳永逸地解决它。 I published a Gist that addresses this issue: https://gist.github.com/nerde/b867cd87d580e97549f2我发表了一个解决这个问题的要点: https ://gist.github.com/nerde/b867cd87d580e97549f2

I created a little AR hack that uses Arel Table to dynamically build the left joins for you, without having to write raw SQL in your code:我创建了一个小 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

Hope it helps.希望能帮助到你。

See below my original post to this question.请参阅下面我对这个问题的原始帖子。

Since then, I have implemented my own .left_joins() for ActiveRecord v4.0.x (sorry, my app is frozen at this version so I've had no need to port it to other versions):从那时起,我为 ActiveRecord v4.0.x 实现了我自己的.left_joins() (对不起,我的应用程序在这个版本中被冻结,所以我不需要将它移植到其他版本):

In file app/models/concerns/active_record_extensions.rb , put the following:在文件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)

Now I can use .left_joins() everywhere I'd normally use .joins() .现在我可以在通常使用 .joins .left_joins()的任何地方使用.joins()

----------------- ORIGINAL POST BELOW ----------------- ----------------- 下面是原帖 -----------------

If you want OUTER JOINs without all the extra eagerly loaded ActiveRecord objects, use .pluck(:id) after .eager_load() to abort the eager load while preserving the OUTER JOIN.如果您希望 OUTER JOIN 没有所有额外急切加载的 ActiveRecord 对象,请在.eager_load() ) 之后使用.pluck(:id)中止急切加载,同时保留 OUTER JOIN。 Using .pluck(:id) thwarts eager loading because the column name aliases ( items.location AS t1_r9 , for example) disappear from the generated query when used (these independently named fields are used to instantiate all the eagerly loaded ActiveRecord objects).使用.pluck(:id)阻止预加载,因为列名别名(例如items.location AS t1_r9 )在使用时会从生成的查询中消失(这些独立命名的字段用于实例化所有预加载的 ActiveRecord 对象)。

A disadvantage of this approach is that you then need to run a second query to pull in the desired ActiveRecord objects identified in the first query:这种方法的一个缺点是您需要运行第二个查询来提取在第一个查询中标识的所需 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)

It'a join query in Active Model in Rails.这是 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

Use Squeel :使用Squeel

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

If anyone out there still needs true left_outer_joins support in Rails 4.2 then if you install the gem "brick" on Rails 4.2.0 or later it automatically adds the Rails 5.0 implementation of left_outer_joins .如果有人在 Rails 4.2 中仍然需要真正的 left_outer_joins 支持,那么如果您在 Rails 4.2.0 或更高版本上安装gem “brick” ,它会自动添加 Rails 5.0 的left_outer_joins实现。 You would probably want to turn off the rest of its functionality, that is unless you want an automatic "admin panel" kind of thing available in your app!您可能想要关闭它的其余功能,除非您想要在您的应用程序中使用自动“管理面板”之类的东西!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM