ЛЕВЫЙ ВНЕШНИЙ ВСТУПИТЕЛЬ В Rails 4

У меня есть 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 - путь, но кажется, что joins() в rails принимает только таблицу как аргумент. 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?

Приветствуется любой ввод.

Ответ 1

Вы также можете передать строку, которая является соединением-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")

Ответ 2

Для этого существует "путь 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
Он делает именно то, что вы просите аккуратным способом.

Ответ 3

Вы выполнили бы запрос как:

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

Если кто-то пришел сюда искать общий способ сделать левое внешнее соединение в Rails 5, вы можете использовать функцию #left_outer_joins.

Пример с несколькими соединениями:

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:

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

Ответ 6

Я довольно долго боролся с подобной проблемой и решил сделать что-то, чтобы решить ее раз и навсегда. Я опубликовал Gist, который решает эту проблему: https://gist.github.com/nerde/b867cd87d580e97549f2

Я создал небольшой AR-хак, который использует таблицу Arel для динамической сборки левых соединений для вас, без необходимости писать сырые 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 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

Надеюсь, что это поможет.

Ответ 7

Используйте Squeel:

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

Ответ 8

Если вы хотите OUTER JOINs без всех дополнительных загруженных объектов ActiveRecord, используйте .pluck(:id) после .eager_load(), чтобы прервать загрузку при сохранении OUTER JOIN. Использование .pluck(:id) thwarts eager loading, потому что псевдонимы столбцов (например, t23 > , например) исчезают из сгенерированного запроса при использовании (эти поля с независимым именем используются для создания экземпляров всех объектов с активной загрузкой 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)

Ответ 9

Объединение includes и where приводит к тому, что 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