ЛЕВОЕ ВНЕШНЕЕ СОЕДИНЕНИЕ в 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

Я хочу запросить список курсов в таблице Courses, которых нет в таблице StudentEnrollments, связанных с определенным студентом.

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

Ответы:

84

Вы можете передать строку, которая также является соединением-sql. напримерjoins("LEFT JOIN StudentEnrollment se ON c.id = se.course_id")

Хотя для ясности я бы использовал стандартные наименования таблиц:

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}" course = Course.active.joins (query) .where (student_enrollments: {id: nil}) Это не такой Rails, как я хотел бы, но он выполняет свою работу. Я пробовал использовать .includes (), который выполняет LEFT JOIN, но не позволяет мне указать дополнительное условие для присоединения. Спасибо, Тарин!
Ханетор
1
Отлично. Эй, иногда мы делаем то, что делаем, чтобы это работало. Пора вернуться к этому и сделать его лучше в будущем ... :)
Taryn East
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
23

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

superuseroi
источник
54
Я просто должен сказать, что не могу поверить, что ActiveRecord все еще не имеет встроенной поддержки для этого после стольких лет. Это совершенно непостижимо.
mrbrdo
1
Sooooo, когда Sequel может стать ORM по умолчанию в Rails?
animatedgif
5
Рельсы не должны раздуваться. Imo они поняли это правильно, когда они решили извлечь драгоценные камни, которые изначально были объединены по умолчанию. Философия - «делай меньше, но хорошо» и «выбирай то, что хочешь»
Адит Саксена,
9
Rails 5 поддерживает LEFT OUTER JOIN: blog.bigbinary.com/2016/03/24/…
Мурад Юсуфов
Чтобы избежать "побочного эффекта" eager_load, см. Мой ответ
textral
13

Объединение includesи whereприводит к тому, что ActiveRecord выполняет ЛЕВОЕ ВНЕШНЕЕ СОЕДИНЕНИЕ за кулисами (без того, где это генерирует обычный набор из двух запросов).

Итак, вы можете сделать что-то вроде:

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

Документы здесь: http://guides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-eager-loaded-associations

Mackshkatz
источник
12

Добавьте к ответу выше, чтобы использовать includes, если вы хотите ВНЕШНЕЕ СОЕДИНЕНИЕ без ссылки на таблицу в where (например, id равняется нулю) или ссылка находится в строке, которую вы можете использовать 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
Любить это! Просто пришлось заменить joinsна, includesи это помогло.
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)
jDmendiola
источник
Вопрос специально нацелен на Rails 4.2.
Volte
6

Вы можете использовать гем left_joins , который возвращает left_joinsметод из Rails 5 для Rails 4 и 3.

Course.left_joins(:student_enrollments)
      .where('student_enrollments.id' => nil)
Хиав Реой
источник
4

Я довольно долго боролся с этой проблемой и решил что-то сделать, чтобы решить ее раз и навсегда. Я опубликовал 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'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 (извините, мое приложение зависло в этой версии, поэтому мне не нужно было переносить его на другие версии):

В файле 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().

----------------- ОРИГИНАЛЬНЫЙ ПОСТ ВНИЗ -----------------

Если вам нужны ВНЕШНИЕ СОЕДИНЕНИЯ без всех дополнительных быстро загружаемых объектов ActiveRecord, используйте .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

Это запрос соединения в Active Model in Rails.

Щелкните здесь, чтобы получить дополнительную информацию о формате запроса активной модели .

@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
джайнвикрам444
источник
3
Лучше добавить пояснения к опубликованному ответу.
Bhushan Kawadkar
3

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

Person.joins{articles.inner}
Person.joins{articles.outer}
Ярин
источник
2
Squeel - неподдерживаемая библиотека, не рекомендуется
iNulty