47

Ich habe 3 Modelle:LEFT OUTER JOIN in Rails 4

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 wünsche ich eine Liste der Kurse in den Kursen Tabelle abzufragen, die in der StudentEnrollments Tabelle nicht existieren, die mit einem bestimmten Schüler verbunden sind, .

Ich fand, dass vielleicht Left Join ist der Weg zu gehen, aber es scheint, dass Joins() in Schienen akzeptieren nur eine Tabelle als Argument. Die SQL-Abfrage, die ich denke, würde das tun, 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 die Rails 4 Weg?

Jede Eingabe wird geschätzt.

Antwort

60

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

Obwohl ich Schienen-Standardtabelle Namensgebung für Klarheit verwenden würde:

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

Meine Lösung endete als: query = "LEFT JOIN student_enrollments ON courses.id = student_enrollments.course_id UND" + "student_enrollments.student_id = # {self.id}" courses = Course.active.joins (Abfrage) .where (student_enrollments: {id: nil}) Es ist nicht so wie Rails, wie ich es will, obwohl es die Arbeit erledigt. Ich habe versucht, .includes() zu verwenden, was die LINKE VERBINDUNG tut, aber es erlaubt mir nicht, eine zusätzliche Bedingung beim Verbinden anzugeben. Danke Taryn! – Khanetor

+0

Großartig. Hey, manchmal machen wir, was wir tun, damit es funktioniert. Zeit, um zu ihr zurückzukehren und sie in Zukunft besser zu machen ... :) –

8

Sie die Abfrage ausführen würde wie:

Course.joins('LEFT JOIN student_enrollment on courses.id = student_enrollment.course_id') 
     .where(active: true, student_enrollments: { student_id: SOME_VALUE, id: nil }) 
4

It'a Abfrage in Active Modell mitmachen Schienen.

Please click here for More info about Active Model Query Format.

@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

Es ist besser, eine Erklärung zu deiner Antwort hinzuzufügen. –

20

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

Sie Arel verwenden könnte, das ist das, was Rails verwendet für ActiveRecrods Abfragen erstellen

Ich würde es in Verfahren wickeln, so dass Sie es gut nennen kann und passieren in welcher Argument 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 die schnelle (und etwas schmutzig) Art und Weise, dass viele Verwendung

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

eager_load funktioniert gut, es muss nur der "Nebeneffekt" von lodging Modellen im Speicher, die Sie möglicherweise nicht benötigen (wie in Ihrem Fall)
Bitte beachten Sie Rails ActiveRecord :: QueryMethods .eager_load
Es tut genau das, was Sie auf eine saubere Art und Weise fragen.

+44

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

+1

Sooooo, wann kann Sequel zum Standard-ORM in Rails werden? – animatedgif

+4

Schienen sollten nicht aufgebläht werden.Imo haben sie es richtig verstanden, als sie beschlossen haben, Edelsteine ​​herauszuziehen, die ursprünglich standardmäßig gebündelt waren. Die Philosophie ist "weniger, aber gut" und "wählen Sie, was Sie wollen" –

3

Ich habe seit einiger Zeit mit dieser Art von Problem gekämpft und beschlossen, etwas zu tun, um es ein für allemal zu lösen. Ich veröffentlichte ein Gist der dieses Problem behebt: https://gist.github.com/nerde/b867cd87d580e97549f2

ich ein wenig AR Hack erstellt, die Arel Tabelle verwendet dynamisch die linke für Sie zu bauen verbindet, ohne rohe SQL in Ihrem Code zu schreiben:

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

Ich habe einen Edelstein aus diesem Code gebaut: https://github.com/ner/left_join – Diego

3

Verwendung Squeel:

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

Squeel ist eine nicht unterstützte Bibliothek , nicht empfohlen – Nultyi

2

Wenn Sie OUTER JOIN möchten, ohne all die zusätzlichen eifrig Active Objekte geladen werden, verwenden .pluck(:id) nach .eager_load() die eifrig Last abzubrechen, während die äußere Erhaltung JOIN. Verwenden Sie .pluck(:id) thwarts Eager laden, da die Spaltennamen Aliase (items.location AS t1_r9, zum Beispiel) von der generierten Abfrage verschwinden, wenn sie verwendet werden (diese unabhängig benannten Felder werden verwendet, um alle begierig geladenen ActiveRecord Objekte zu instanziieren).

Ein Nachteil dieses Ansatzes ist, dass Sie dann eine zweite Abfrage ausführen müssen in den gewünschten Active Objekte in der ersten Abfrage identifiziert ziehen:

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

Wenn jemand hier kam eine generische Art und Weise suchen sie eine linke äußere Verknüpfung in Rails 5 zu tun, können Sie die #left_outer_joins verwenden Funktion.

Multi-Join Beispiel:

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 
+0

Danke, ich möchte für die Kreuzverknüpfung linken äußeren Joins erwähnen, verwenden Sie 'left_outer_joins (a: [: b,: c])' – fangxing

1

oben auf die Antwort Hinzufügen, includes zu verwenden, wenn Sie eine OUTER ohne die Referenzierung beitreten möchten Tabelle in der where (wie ID ist Nil) oder die Referenz in einer Zeichenfolge können Sie references verwenden. Das würde wie folgt 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