UNIÓN EXTERIOR IZQUIERDA en rieles 4

80

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.

Khanetor
fuente
Si el registro no existe en StudentEnrollments, ¿seguramente se.student_id = <SOME_STUDENT_ID_VALUE>sería imposible?
PJSCopeland

Respuestas:

84

También puede pasar una cadena que sea join-sql. p.ejjoins("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")
Taryn East
fuente
2
Mi solución terminó siendo: query = "LEFT JOIN student_enrollments ONourses.id = student_enrollments.course_id AND" + "student_enrollments.student_id = # {self.id} "ourses = Course.active.joins (query) .where (student_enrollments: {id: nil}) No es tan Rails como quiero que sea, aunque hace el trabajo. Intenté usar .includes (), que hace la LEFT JOIN, pero no me permite especificar una condición adicional al unirme. ¡Gracias Taryn!
Khanetor
1
Excelente. Oye, a veces hacemos lo que hacemos para que funcione. Es hora de volver a él y mejorarlo en el futuro ... :)
Taryn East
1
@TarynEast "Hazlo funcionar, hazlo rápido, hazlo hermoso". :)
Joshua Pinter
31

Si alguien vino aquí buscando una forma genérica de hacer una combinación externa izquierda en Rails 5, puede usar la #left_outer_joinsfunció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
Blaskovicz
fuente
1
Gracias, quiero mencionar las combinaciones externas izquierdas de asociación cruzada, usoleft_outer_joins(a: [:b, :c])
fangxing
También lo tienes disponible left_joinspara abreviar y comportarte del mismo modo. P.ej. left_joins(:order_reports)
alexventuraio
23

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.

superuseroi
fuente
54
Solo tengo que decir que no puedo creer que ActiveRecord todavía no tenga soporte integrado para esto después de tantos años. Es completamente insondable.
mrbrdo
1
Entonces, ¿cuándo puede Sequel convertirse en el ORM predeterminado en Rails?
animatedgif
5
Los rieles no deben hincharse. Imo lo hicieron bien cuando decidieron extraer gemas que estaban empaquetadas por defecto en primer lugar. La filosofía es "hacer menos pero bien" y "elegir lo que quiera"
Adit Saxena
9
Rails 5 tiene soporte para LEFT OUTER JOIN: blog.bigbinary.com/2016/03/24/…
Murad Yusufov
Para evitar el "efecto secundario" de eager_load, vea mi respuesta
textral
12

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 usar references. 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

Jonathon Gardner
fuente
¿Funcionará esto para una relación profundamente anidada o la relación debe colgarse directamente del modelo que se está consultando? Parece que no puedo encontrar ningún ejemplo de lo primero.
dps
¡Quiéralo! Sólo tuvo que sustituir joinspor includesy lo hizo el truco.
RaphaMex
8

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 })
Joe Kennedy
fuente
7

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)
jDmendiola
fuente
La pregunta se dirige específicamente a Rails 4.2.
Volte
6

Puede usar la gema left_joins , que hace un backports del left_joinsmétodo de Rails 5 para Rails 4 y 3.

Course.left_joins(:student_enrollments)
      .where('student_enrollments.id' => nil)
khiav reoy
fuente
4

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.

Diego
fuente
4

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_r9por 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)
textral
fuente
Esto es interesante.
dps
+1 pero puede mejorar un poco más y usar en select(:id)lugar de pluck(:id)y evitar que se materialice la consulta interna, y dejarlo todo en la base de datos.
Andre Figueiredo
3

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
jainvikram444
fuente
3
Es mejor agregar alguna explicación a su respuesta publicada.
Bhushan Kawadkar
3

Utilice Squeel :

Person.joins{articles.inner}
Person.joins{articles.outer}
Yarin
fuente
2
Squeel es una biblioteca no compatible, no se recomienda
iNulty