LEWE ZEWNĘTRZNE POŁĄCZENIE w szynach 4

80

Mam 3 modele:

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

Chcę zapytać o listę kursów w tabeli Kursy, które nie istnieją w tabeli StudentEnrollments, które są powiązane z określonym studentem.

Odkryłem, że być może Left Join jest właściwą drogą, ale wydaje się, że joins () w railsach akceptuje tylko tabelę jako argument. Zapytanie SQL, które moim zdaniem zrobiłoby to, czego chcę, to:

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

Jak wykonać to zapytanie sposobem Rails 4?

Każdy wkład jest mile widziany.

Khanetor
źródło
Jeśli rekord nie istnieje w StudentEnrollments, na pewno se.student_id = <SOME_STUDENT_ID_VALUE>byłby niemożliwy?
PJSCopeland

Odpowiedzi:

84

Możesz również przekazać łańcuch, który jest złączeniem sql. na przykładjoins("LEFT JOIN StudentEnrollment se ON c.id = se.course_id")

Chociaż użyłbym standardowego nazewnictwa tabel dla przejrzystości:

joins("LEFT JOIN student_enrollments ON courses.id = student_enrollments.course_id")
Taryn East
źródło
2
Moje rozwiązanie to: query = "LEFT JOIN student_enrollments ONourses.id = student_enrollments.course_id AND" + "student_enrollments.student_id = # {self.id}" tours = Course.active.joins (query) .where (student_enrollments: {id: nil}) To nie jest tak Railsowe, jak bym chciał, chociaż wykonuje swoją pracę. Próbowałem użyć .includes (), który wykonuje LEFT JOIN, ale nie pozwala mi określić dodatkowego warunku łączenia. Dzięki Taryn!
Khanetor
1
Świetny. Hej, czasami robimy to, co robimy, żeby to działało. Czas wrócić do tego i uczynić go lepszym w przyszłości ... :)
Taryn East
1
@TarynEast "Zrób to, zrób to szybko, zrób to pięknie." :)
Joshua Pinter
31

Jeśli ktoś przyszedł tutaj i szukał ogólnego sposobu na wykonanie lewego zewnętrznego sprzężenia w Rails 5, może użyć #left_outer_joins funkcji.

Przykład wielokrotnego łączenia:

Rubin:

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
Blaskovicz
źródło
1
Dzięki, chcę wspomnieć o skojarzeniu krzyżowym lewych połączeń zewnętrznych, użyjleft_outer_joins(a: [:b, :c])
fangxing
Masz również dostępne left_joinskrótkie i zachowujesz się w ten sam sposób. Na przykład. left_joins(:order_reports)
alexventuraio
23

Właściwie jest na to „sposób na szyny”.

Możesz użyć Arela , którego Railsy używają do tworzenia zapytań dla ActiveRecrods

Owinąłbym to w metodę, abyś mógł ją ładnie nazwać i przekazać dowolny argument, na przykład:

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

Istnieje również szybki (i nieco brudny) sposób, z którego korzysta wielu

Course.eager_load(:students).where(
    student_enrollments: {student_id: some_user.id, id: nil}, 
    active: true
)

eager_load działa świetnie, ma po prostu "efekt uboczny" umieszczania modeli w pamięci, których możesz nie potrzebować (jak w twoim przypadku).
Zobacz Rails ActiveRecord :: QueryMethods .eager_load
Robi dokładnie to, o co prosisz w schludny sposób.

superuseroi
źródło
54
Muszę tylko powiedzieć, że nie mogę uwierzyć, że ActiveRecord nadal nie ma wbudowanej obsługi tego po tylu latach. To zupełnie niezgłębione.
mrbrdo
1
Sooooo, kiedy Sequel może stać się domyślnym ORM w Railsach?
animatedgif
5
Szyny nie powinny się nadymać. Imo, zrozumieli to dobrze, kiedy zdecydowali się wydobyć klejnoty, które były domyślnie pakowane w pierwszej kolejności. Filozofia to „rób mniej, ale dobrze” i „wybieraj, co chcesz”
Sztolnia Saxena
9
Rails 5 obsługuje LEFT OUTER JOIN: blog.bigbinary.com/2016/03/24/…
Murad Yusufov
Aby uniknąć "efektu ubocznego" eager_load, zobacz moją odpowiedź
textral
12

Dodając do powyższej odpowiedzi, użyj includes, jeśli chcesz, aby JOIN ZEWNĘTRZNY bez odwoływania się do tabeli w miejscu (np. Id jest zerowy) lub odniesienie jest w ciągu, którego możesz użyć references. To wyglądałoby tak:

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

lub

Course.includes(:student_enrollments).references(:student_enrollments).where('student_enrollments.id = ?', nil)

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

Jonathon Gardner
źródło
Czy to zadziała dla głęboko zagnieżdżonych relacji, czy też relacja musi wisieć bezpośrednio na modelu, którego dotyczy zapytanie? Nie mogę znaleźć żadnych przykładów tego pierwszego.
dps
Kocham to! Wystarczyło wymienić joinsna includesi to załatwiło sprawę.
RaphaMex
8

Wykonałbyś zapytanie jako:

Course.joins('LEFT JOIN student_enrollment on courses.id = student_enrollment.course_id')
      .where(active: true, student_enrollments: { student_id: SOME_VALUE, id: nil })
Joe Kennedy
źródło
7

Wiem, że to stare pytanie i stary wątek, ale w Railsach 5 możesz po prostu to zrobić

Course.left_outer_joins(:student_enrollments)
jDmendiola
źródło
Pytanie dotyczy w szczególności Railsów 4.2.
Volte
6

Możesz użyć gem left_joins , który jest left_joinsmetodą backportów z Rails 5 dla Rails 4 i 3.

Course.left_joins(:student_enrollments)
      .where('student_enrollments.id' => nil)
khiav reoy
źródło
4

Od jakiegoś czasu zmagam się z tego rodzaju problemem i postanowiłem zrobić coś, aby rozwiązać go raz na zawsze. Opublikowałem streszczenie, które dotyczy tego problemu: https://gist.github.com/nerde/b867cd87d580e97549f2

Stworzyłem mały hack AR, który używa Arel Table do dynamicznego budowania lewych złączeń, bez konieczności pisania surowego SQL w kodzie:

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

Mam nadzieję, że to pomoże.

Diego
źródło
4

Zobacz poniżej mój oryginalny post na to pytanie.

Od tego czasu wdrażam własne .left_joins() dla ActiveRecord v4.0.x (przepraszam, moja aplikacja jest zawieszona w tej wersji, więc nie musiałem przenosić jej do innych wersji):

W pliku app/models/concerns/active_record_extensions.rbumieść następujące informacje:

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)

Teraz mogę używać .left_joins()wszędzie tam, gdzie normalnie używam.joins() .

----------------- ORYGINALNY POST PONIŻEJ -----------------

Jeśli chcesz, aby OUTER JOINs nie zawierały wszystkich dodatkowych, chętnie ładowanych obiektów ActiveRecord, użyj .pluck(:id)after, .eager_load()aby przerwać gorące ładowanie, zachowując jednocześnie OUTER JOIN. Użycie .pluck(:id)uniemożliwia przyspieszone ładowanie, ponieważ aliasy nazw kolumn ( items.location AS t1_r9na przykład) znikają z wygenerowanego zapytania, gdy są używane (te niezależnie nazwane pola są używane do tworzenia instancji wszystkich chętnie ładowanych obiektów ActiveRecord).

Wadą tego podejścia jest to, że trzeba następnie uruchomić drugie zapytanie, aby pobrać żądane obiekty ActiveRecord zidentyfikowane w pierwszym zapytaniu:

# 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)
textral
źródło
To jest interesujące.
dps
+1, ale możesz ulepszyć trochę więcej i używać select(:id)zamiast pluck(:id)i zapobiegać materializacji wewnętrznego zapytania i pozostawieniu tego wszystkiego w bazie danych.
Andre Figueiredo
3

Użyj Squeel :

Person.joins{articles.inner}
Person.joins{articles.outer}
Yarin
źródło
2
Squeel to nieobsługiwana biblioteka, niezalecana
iNulty