在Rails 4中向左外联接


80

我有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

我希望在“课程”表中查询课程列表,该列表在与某个学生相关联的“学生录取”表中不存在。

我发现也许使用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 = <SOME_STUDENT_ID_VALUE>是不可能的吗?
PJSCopeland '18年

Answers:


84

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

尽管为了清楚起见,我将使用rails-standard表命名:

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

2
我的解决方案最终是:query =“ LEFT JOIN student_enrollments on course.id = student_enrollments.course_id AND” +“ student_enrollments.student_id =#{self.id}”“ courses = Course.active.joins(query).where(student_enrollments: {id:nil})虽然可以完成工作,但它不像我想要的那样成为Rails。我尝试使用.includes()进行左联接,但是它不允许我指定联接的额外条件。谢谢塔琳!
Khanetor 2014年

1
大。嘿,有时候我们会做些什么才能使其正常工作。是时候回到它,并在未来做得更好了... :)
Taryn East 2014年

1
@TarynEast“使它工作,使其快速,使其美观。” :)
约书亚·品特

31

如果有人来这里寻找在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

1
谢谢,我要提到交叉关联的左外部连接,使用left_outer_joins(a: [:b, :c])
fangxing

此外,您还可以left_joins使用简短的内容并以相同的方式进行操作。例如。left_joins(:order_reports)
alexventuraio

22

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

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

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

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
它以一种整洁的方式完全满足您的要求。


54
我只能说我不相信ActiveRecord在这么多年后仍然没有内置的支持。这是完全不可思议的。
mrbrdo 2015年

1
那么Sequel何时才能成为Rails中的默认ORM?
动画

5
Rails不应become肿。当他们决定提取默认情况下捆绑在一起的宝石时,Imo便做到了正确。经营理念是“少做但善”和“随心所欲”
Adit Saxena

9
导轨5有LEFT OUTER JOIN支持:blog.bigbinary.com/2016/03/24/...
穆拉德Yusufov

为避免eager_load的“副作用”,请参阅我的答案
textral


12

要在上面添加答案,请使用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


这对于深度嵌套的关系是否起作用,或者该关系是否需要直接挂在要查询的模型之外?我似乎找不到前者的任何例子。
dps

爱它!刚刚更换joinsincludes,它的伎俩。
RaphaMex

8

您可以按以下方式执行查询:

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

7

我知道这是一个古老的问题,一个古老的话题,但是在Rails 5中,您可以简单地做

Course.left_outer_joins(:student_enrollments)

这个问题专门针对Rails 4.2。
Volte

6

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

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

4

我已经在这种问题上苦苦挣扎了很长时间,因此决定做一些事情来一劳永逸地解决它。我发布了解决此问题的要点: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

希望能帮助到你。


4

请参阅以下我对此问题的原始帖子。

从那时起,我就.left_joins()为ActiveRecord v4.0.x实现了自己的版本(对不起,我的应用程序已冻结在该版本上,因此我无需将其移植到其他版本):

在file中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)

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

-----------------以下原始帖子-----------------

如果您希望OUTER JOINs没有所有多余的ActiveRecord对象,则在保留OUTER JOIN的同时使用.pluck(:id)after.eager_load()中止急切的加载。使用.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)

这是有趣的。
dps

+1,但您可以提高一点,并使用select(:id)代替pluck(:id)并防止实现内部查询,并将其全部留给数据库。
安德烈·菲盖瑞多


3

使用Squeel

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

2
Squeel是不受支持的库,不建议使用
iNulty
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.