2014-06-23 14 views
47

ho 3 modelli: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 

desidero query per un elenco di corsi nella tabella I corsi, che non esiste nella tabella StudentEnrollments che sono associati con un certo studente .

Ho scoperto che forse Left Join è la strada da percorrere, ma sembra che joins() in rails accetti solo una tabella come argomento. La query SQL che penso avrebbe fatto quello che voglio è:

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 

Come faccio a eseguire questa query il Rails 4 vie?

Qualsiasi input è apprezzato.

risposta

60

È possibile passare una stringa che è anche join-sql. ad esempio joins("LEFT JOIN StudentEnrollment se ON c.id = se.course_id")

Anche se mi piacerebbe utilizzare le rotaie standard tabella di denominazione per la chiarezza:

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

La mia soluzione ha finito per essere: query = "LEFT JOIN student_enrollments ON courses.id = student_enrollments.course_id e" + "student_enrollments.student_id = # {self.id}" courses = Course.active.joins (query) .where (student_enrollments: {id: nil}) Non è come Rails come voglio, anche se ha fatto il lavoro. Ho provato a usare .includes(), che fa il LEFT JOIN, ma non mi permette di specificare una condizione aggiuntiva al momento dell'iscrizione. Grazie Taryn! – Khanetor

+0

Grande. Ehi, a volte facciamo quello che facciamo per farlo funzionare. È ora di tornare a farlo e renderlo migliore in futuro ... :) –

8

Faresti esegue la query come:

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 unirsi query nel modello attivo in Rails.

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

È meglio aggiungere qualche spiegazione alla risposta inviata. –

20

Esiste effettivamente un "Modo Rotaie" per fare ciò.

Si potrebbe utilizzare Arel, che è quello che utilizza Rails per costruire query per ActiveRecrods

avrei avvolgerla in metodo in modo che si può chiamare bene e passare in qualsiasi argomento che si desidera, qualcosa di simile a:

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 

V'è anche il rapido (e un po 'sporco) modo in cui molti usano

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

eager_load grandi opere, che ha appena l '"effetto collaterale" dei modelli di memoria in memoria che potresti non aver bisogno (come nel tuo caso)
Vedi Rails ActiveRecord :: QueryMethods .eager_load
Fa esattamente quello che stai chiedendo in modo pulito.

+44

Devo solo dire che non posso credere che ActiveRecord non abbia ancora un supporto integrato per questo dopo così tanti anni. È completamente incomprensibile. – mrbrdo

+1

Sooooo quando può Sequel diventare l'ORM predefinito in Rails? – animatedgif

+4

I binari non devono gonfiarsi.Imo hanno capito bene quando hanno deciso di estrarre gemme che erano in bundle di default in primo luogo. La filosofia è "fai meno ma bene" e "scegli ciò che vuoi" –

3

Ho sofferto con questo tipo di problema da un po 'di tempo e ho deciso di fare qualcosa per risolverlo una volta per tutte. Ho pubblicato un Gist che risolve questo problema: https://gist.github.com/nerde/b867cd87d580e97549f2

ho creato un piccolo hack AR che utilizza Arel Table per costruire dinamicamente la sinistra si unisce per voi, senza dover scrivere SQL prime nel codice:

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 

Spero che sia d'aiuto.

+4

Ho creato una gemma da questo codice: https://github.com/nerde/left_join – Diego

3

Uso Squeel:

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

Squeel è una libreria non supportata , non consigliato – Nultyi

2

Se volete OUTER join senza tutti gli oggetti extra avidamente caricati ActiveRecord, utilizzare .pluck(:id) dopo .eager_load() per interrompere il carico desiderosi preservando l'OUTER JOIN. L'uso di stacking .pluck(:id) richiede il caricamento perché gli alias del nome della colonna (items.location AS t1_r9, ad esempio) scompaiono dalla query generata quando vengono utilizzati (questi campi denominati in modo indipendente vengono utilizzati per creare un'istanza di tutti gli oggetti ActiveRecord caricati con entusiasmo).

Uno svantaggio di questo approccio è che è quindi necessario eseguire una seconda query per tirare negli oggetti ActiveRecord desiderati identificati nella prima query:

# 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

Se qualcuno è venuto qui in cerca di un modo generico per fare un join esterno sinistro in Rails 5, è possibile utilizzare il #left_outer_joins funzione.

Multi-join esempio:

Rubino:

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

Grazie, voglio menzionare per i join esterni a sinistra delle associazioni incrociate, usa 'left_outer_joins (a: [: b,: c])' – fangxing

1

Aggiungendo alla risposta di cui sopra, per usare includes, se si vuole un outer join senza fare riferimento alla tabella nel dove (come id è nullo) o il riferimento è in una stringa è possibile utilizzare references. Che sarebbe simile a questa:

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

o

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

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