Tengo 3 modelos:
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
Deseo consultar una lista de cursos en la tabla Cursos, que no existen en la tabla StudentEnrollments que están asociados con un estudiante determinado.
Descubrí que quizás Left Join es el camino a seguir, pero parece que joins () en rieles solo aceptan una tabla como argumento. La consulta SQL que creo que haría lo que quiero es:
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
¿Cómo ejecuto esta consulta de la manera Rails 4?
Se agradece cualquier aporte.
se.student_id = <SOME_STUDENT_ID_VALUE>
sería imposible?Respuestas:
También puede pasar una cadena que sea join-sql. p.ej
joins("LEFT JOIN StudentEnrollment se ON c.id = se.course_id")
Aunque usaría nombres de tablas estándar de rieles para mayor claridad:
joins("LEFT JOIN student_enrollments ON courses.id = student_enrollments.course_id")
fuente
Si alguien vino aquí buscando una forma genérica de hacer una combinación externa izquierda en Rails 5, puede usar la
#left_outer_joins
función.Ejemplo de combinación múltiple:
Rubí:
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
fuente
left_outer_joins(a: [:b, :c])
left_joins
para abreviar y comportarte del mismo modo. P.ej.left_joins(:order_reports)
En realidad, existe una "forma de rieles" para hacer esto.
Podría usar Arel , que es lo que usa Rails para construir consultas para ActiveRecrods
Lo envolvería en el método para que pueda llamarlo amablemente y pasar el argumento que desee, algo como:
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
También existe la forma rápida (y ligeramente sucia) que muchos usan
Course.eager_load(:students).where( student_enrollments: {student_id: some_user.id, id: nil}, active: true )
eager_load funciona muy bien, solo tiene el "efecto secundario" de cargar modelos en la memoria que quizás no necesite (como en su caso).
Consulte Rails ActiveRecord :: QueryMethods .eager_load
Hace exactamente lo que está pidiendo de una manera ordenada.
fuente
Combinando
includes
ywhere
da resultado que ActiveRecord realice una LEFT OUTER JOIN detrás de escena (sin el lugar donde esto generaría el conjunto normal de dos consultas).Entonces podrías hacer algo como:
Course.includes(:student_enrollments).where(student_enrollments: { course_id: nil })
Documentos aquí: http://guides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-eager-loaded-associations
fuente
Agregando a la respuesta anterior, para usar
includes
, si desea un OUTER JOIN sin hacer referencia a la tabla en el dónde (como id siendo nil) o la referencia está en una cadena que puede usarreferences
. Eso se vería así: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
fuente
joins
porincludes
y lo hizo el truco.Ejecutaría la consulta como:
Course.joins('LEFT JOIN student_enrollment on courses.id = student_enrollment.course_id') .where(active: true, student_enrollments: { student_id: SOME_VALUE, id: nil })
fuente
Sé que esta es una pregunta antigua y un hilo antiguo, pero en Rails 5, simplemente podrías hacer
Course.left_outer_joins(:student_enrollments)
fuente
Puede usar la gema left_joins , que hace un backports del
left_joins
método de Rails 5 para Rails 4 y 3.Course.left_joins(:student_enrollments) .where('student_enrollments.id' => nil)
fuente
He estado luchando con este tipo de problema durante bastante tiempo y decidí hacer algo para resolverlo de una vez por todas. Publiqué un Gist que aborda este problema: https://gist.github.com/nerde/b867cd87d580e97549f2
Creé un pequeño truco de AR que usa Arel Table para construir dinámicamente las uniones izquierdas para usted, sin tener que escribir SQL sin formato en su código:
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
Espero eso ayude.
fuente
Vea a continuación mi publicación original a esta pregunta.
Desde entonces, he implementado la mía propia
.left_joins()
para ActiveRecord v4.0.x (lo siento, mi aplicación está congelada en esta versión, por lo que no tuve necesidad de migrarla a otras versiones):En archivo
app/models/concerns/active_record_extensions.rb
, ponga lo siguiente: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)
Ahora puedo usar en
.left_joins()
todos los lugares que normalmente usaría.joins()
.----------------- POSTE ORIGINAL ABAJO -----------------
Si desea OUTER JOINs sin todos los objetos ActiveRecord cargados con entusiasmo, use
.pluck(:id)
after.eager_load()
para abortar la carga ansiosa mientras conserva OUTER JOIN. El uso.pluck(:id)
frustra la carga ansiosa porque los alias de los nombres de las columnas (items.location AS t1_r9
por ejemplo) desaparecen de la consulta generada cuando se usan (estos campos con nombres independientes se usan para instanciar todos los objetos ActiveRecord cargados ansiosamente).Una desventaja de este enfoque es que luego debe ejecutar una segunda consulta para extraer los objetos ActiveRecord deseados identificados en la primera consulta:
# 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)
fuente
select(:id)
lugar depluck(:id)
y evitar que se materialice la consulta interna, y dejarlo todo en la base de datos.Es una consulta de combinación en Active Model in Rails.
Haga clic aquí para obtener más información sobre el formato de consulta de modelo activo .
@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
fuente
Utilice Squeel :
Person.joins{articles.inner} Person.joins{articles.outer}
fuente