LINKS AUSSEN VERBINDEN in Schienen 4


80

Ich habe 3 Modelle:

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

Ich möchte eine Liste von Kursen in der Tabelle "Kurse" abfragen, die in der Tabelle "StudentEnrollments" nicht vorhanden sind und einem bestimmten Studenten zugeordnet sind.

Ich fand, dass Left Join vielleicht der richtige Weg ist, aber es scheint, dass joins () in Rails nur eine Tabelle als Argument akzeptiert. Die SQL-Abfrage, von der ich denke, dass sie das tun würde, was ich will, ist:

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

Wie führe ich diese Abfrage auf Rails 4-Weise aus?

Jede Eingabe wird geschätzt.


Wenn der Datensatz in StudentEnrollments nicht vorhanden ist, se.student_id = <SOME_STUDENT_ID_VALUE>wäre dies sicherlich unmöglich?
PJSCopeland

Antworten:


84

Sie können eine Zeichenfolge übergeben, die auch die Join-SQL ist. z.Bjoins("LEFT JOIN StudentEnrollment se ON c.id = se.course_id")

Obwohl ich aus Gründen der Klarheit die Schienen-Standardtabellennamen verwenden würde:

joins("LEFT JOIN student_enrollments ON courses.id = student_enrollments.course_id")

2
Meine Lösung lautete: query = "LEFT JOIN student_enrollments ON classes.id = student_enrollments.course_id AND" + "student_enrollments.student_id = # {self.id}" classes = Course.active.joins (query) .where (student_enrollments: {id: nil}) Es ist nicht so Rails, wie ich es mir wünsche, obwohl es die Arbeit erledigt. Ich habe versucht, .includes () zu verwenden, das LEFT JOIN ausführt, aber ich kann keine zusätzliche Bedingung für das Beitreten angeben. Danke Taryn!
Khanetor

1
Toll. Hey, manchmal tun wir das, was wir tun, damit es funktioniert. Zeit, darauf zurückzukommen und es in Zukunft besser zu machen ... :)
Taryn East

1
@TarynEast "Lass es funktionieren, mach es schnell, mach es schön." :)
Joshua Pinter

31

Wenn jemand hierher gekommen ist, um nach einer generischen Möglichkeit zu suchen, eine linke äußere Verknüpfung in Rails 5 durchzuführen, können Sie die #left_outer_joinsFunktion verwenden.

Beispiel für mehrere Verknüpfungen:

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

1
Vielen Dank, ich möchte für die left_outer_joins(a: [:b, :c])
Kreuzassoziation

Auch Sie haben left_joinsfür kurze Zeit zur Verfügung und verhalten sich genauso. Z.B. left_joins(:order_reports)
Alexventuraio

22

Es gibt tatsächlich einen "Rails Way", um dies zu tun.

Sie können Arel verwenden , mit dem Rails Abfragen für ActiveRecrods erstellt

Ich würde es in eine Methode einwickeln, damit Sie es schön aufrufen und jedes Argument übergeben können, das Sie möchten, so etwas wie:

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

Es gibt auch den schnellen (und leicht schmutzigen) Weg, den viele benutzen

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

eifrig_load funktioniert großartig, es hat nur den "Nebeneffekt", dass Modelle im Speicher gespeichert werden, die Sie möglicherweise nicht benötigen (wie in Ihrem Fall).
Siehe Rails ActiveRecord :: QueryMethods .eager_load
Es macht genau das, was Sie fragen, auf eine ordentliche Art und Weise.


54
Ich muss nur sagen, dass ich nicht glauben kann, dass ActiveRecord nach so vielen Jahren noch keine integrierte Unterstützung dafür hat. Es ist völlig unergründlich.
Mrbrdo

1
Sooooo wann kann Sequel zum Standard-ORM in Rails werden?
animiertes

5
Schienen sollten nicht aufgebläht werden. Imo haben sie es richtig gemacht, als sie beschlossen, Edelsteine ​​herauszuholen, die standardmäßig gebündelt wurden. Die Philosophie ist "weniger tun, aber gut" und "wählen, was Sie wollen"
Adit Saxena

9
Rails 5 hat Unterstützung für LEFT OUTER JOIN: blog.bigbinary.com/2016/03/24/…
Murad Yusufov

Um den "Nebeneffekt" von eifrig_load zu vermeiden, siehe meine Antwort
textral


12

Hinzufügen zu der obigen Antwort, um zu verwenden includes, wenn Sie einen OUTER JOIN möchten, ohne auf die Tabelle im where zu verweisen (wie id ist nil) oder die Referenz in einer Zeichenfolge ist, die Sie verwenden können references. Das würde so aussehen:

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

oder

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

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


Funktioniert dies für eine tief verschachtelte Beziehung oder muss die Beziehung direkt vom abgefragten Modell abhängen? Ich kann anscheinend keine Beispiele für Ersteres finden.
dps

Liebe es! Wir hatten gerade zu ersetzen joinsdurch includesund es hat den Trick.
RaphaMex

8

Sie würden die Abfrage wie folgt ausführen:

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

Ich weiß, dass dies eine alte Frage und ein alter Thread ist, aber in Rails 5 können Sie dies einfach tun

Course.left_outer_joins(:student_enrollments)

Die Frage richtet sich speziell an Rails 4.2.
Volte


4

Ich habe seit einiger Zeit mit solchen Problemen zu kämpfen und beschlossen, etwas zu tun, um sie ein für alle Mal zu lösen. Ich habe eine Übersicht veröffentlicht, die dieses Problem behebt : https://gist.github.com/nerde/b867cd87d580e97549f2

Ich habe einen kleinen AR-Hack erstellt, der Arel Table verwendet, um die linken Joins dynamisch für Sie zu erstellen, ohne dass Sie unformatiertes SQL in Ihren Code schreiben müssen:

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

Ich hoffe es hilft.


4

Siehe unten meinen ursprünglichen Beitrag zu dieser Frage.

Seitdem habe ich meine eigene .left_joins()für ActiveRecord v4.0.x implementiert (leider ist meine App in dieser Version eingefroren, sodass ich sie nicht auf andere Versionen portieren musste):

Fügen Sie app/models/concerns/active_record_extensions.rbin die Datei Folgendes ein:

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)

Jetzt kann ich .left_joins()überall verwenden, wo ich normalerweise verwenden würde .joins().

----------------- ORIGINAL POST UNTEN -----------------

Wenn Sie OUTER JOINs ohne alle besonders eifrig geladenen ActiveRecord-Objekte möchten, verwenden Sie .pluck(:id)after .eager_load(), um das eifrige Laden abzubrechen, während Sie OUTER JOIN beibehalten. Verwenden Sie das .pluck(:id)eifrige Laden, da die Spaltennamen-Aliase ( items.location AS t1_r9zum Beispiel) bei Verwendung aus der generierten Abfrage verschwinden (diese unabhängig benannten Felder werden verwendet, um alle eifrig geladenen ActiveRecord-Objekte zu instanziieren).

Ein Nachteil dieses Ansatzes besteht darin, dass Sie dann eine zweite Abfrage ausführen müssen, um die gewünschten ActiveRecord-Objekte abzurufen, die in der ersten Abfrage identifiziert wurden:

# 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)

Das ist interessant.
dps

+1, aber Sie können ein wenig mehr verbessern und select(:id)anstelle der pluck(:id)Verhinderung der Materialisierung innerer Abfragen verwenden und alles der Datenbank überlassen.
Andre Figueiredo


3

Verwenden Sie Squeel :

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

2
Squeel ist eine nicht unterstützte Bibliothek, nicht empfohlen
iNulty
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.