ЛІВО ВНЕШНЄ ПРИЄДНАННЯ в 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

Я хочу запитати список курсів у таблиці Курси, які не існують у таблиці 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?

Будь-який внесок вітається.


Якщо запис не існує в StudentEnrollments, то, безумовно, це se.student_id = <SOME_STUDENT_ID_VALUE>було б неможливо?
ПАТ "Копеленд",

Відповіді:


84

Ви також можете передати рядок, який є об'єднанням-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
Моє рішення закінчилось таким: query = "ПРИЄДНАЙТЕСЬ ДО ПРИСТРОЮ student_enrollments ON courses.id = student_enrollments.course_id І" + "student_enrollments.student_id = # {self.id}" courses = Course.active.joins (query) .where (students_enrollments: {id: nil}) Це не такий Rails, як я хочу, хоча це робить роботу. Я спробував використовувати .includes (), який робить LEFT JOIN, але це не дозволяє мені вказати додаткову умову при приєднанні. Дякую Тарин!
Ханетор

1
Чудово. Гей, іноді ми робимо те, що робимо, щоб це працювало. Час повернутися до нього та покращити його в майбутньому ... :)
Тарін Іст

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

Для цього насправді існує "Рейковий шлях".

Ви можете використовувати 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
Він робить саме те, про що ви запитуєте, акуратно.


54
Я просто повинен сказати, що не можу повірити, що ActiveRecord досі не має вбудованої підтримки для цього через стільки років. Це абсолютно незбагненно.
mrbrdo

1
Ну що ж, коли Sequel може стати ORM за замовчуванням у Rails?
animatedgif

5
Рейки не повинні роздуватися. Імо, вони зрозуміли це правильно, коли вирішили видобувати дорогоцінні камені, які спочатку були в комплекті. Філософія: "робіть менше, але добре" і "вибирайте, що хочете"
Адіт Саксена

9
Rails 5 має підтримку ЛІВОГО ВНЕШНЬОГО ПРИЄДНАННЯ
Мурад Юсуфов

Щоб уникнути "побічного ефекту" 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


12

Додавання до відповіді вище, щоб використовувати includes, якщо ви хочете ЗОВНІШНЄ ПРИЄДНАННЯ без посилання на таблицю в де (наприклад, ідентифікатор нуль) або посилання знаходиться у рядку, який ви можете використовувати 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)

Питання стосується конкретно 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 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 (вибачте, моя програма заморожена в цій версії, тому мені не потрібно було переносити її на інші версії):

У файл 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)після, .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

3
Краще додати якесь пояснення до своєї опублікованої відповіді.
Bhushan Kawadkar

3

Використовуйте Squeel :

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

2
Squeel - непідтримувана бібліотека, не рекомендується
iNulty
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.