Rails 4中的LEFT OUTER JOIN

时间:2014-06-23 05:18:47

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

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

我希望查询课程表中的课程列表,这些课程在StudentEnrollments表中与特定学生相关联。

我发现也许Left Join是要走的路,但似乎rails中的join()只接受一个表作为参数。 我认为可以做我想做的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的方式执行此查询?

赞赏任何意见。

12 个答案:

答案 0 :(得分:77)

您也可以传递一个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")

答案 1 :(得分:23)

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

答案 2 :(得分:20)

实际上有一种&#34; Rails Way&#34;去做这个。

您可以使用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工作得很好,只有副作用&#34;记忆中你可能不需要的模型(如你的情况) 请参阅Rails ActiveRecord :: QueryMethods .eager_load
它完全按照你要求的方式完成。

答案 3 :(得分:10)

组合includeswhere会导致ActiveRecord在幕后执行LEFT OUTER JOIN(没有这将生成两个查询的正常集合)。

所以你可以这样做:

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

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

答案 4 :(得分: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 })

答案 5 :(得分:8)

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

references

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

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

答案 6 :(得分:5)

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

Course.left_outer_joins(:student_enrollments)

答案 7 :(得分:4)

它是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

答案 8 :(得分:4)

我在很长一段时间内一直在努力解决这类问题,并决定一劳永逸地解决这个问题。我发布了一个解决此问题的要点:https://gist.github.com/nerde/b867cd87d580e97549f2

我创建了一个使用Arel Table来动态构建左连接的AR hack,而无需在代码中编写原始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

希望它有所帮助。

答案 9 :(得分:3)

使用Squeel

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

答案 10 :(得分:3)

如果您想要OUTER JOIN而没有所有额外的热切加载的ActiveRecord对象,请在.pluck(:id)之后使用.eager_load()中止预先加载,同时保留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)

答案 11 :(得分:3)

您可以使用left_joins gem,它将Rails 5中的left_joins方法用于Rails 4和3。

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