UNIÓN EXTERIOR IZQUIERDA en Raíles 4


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 determinado estudiante.

He encontrado que quizás Left Join es el camino a seguir, pero parece que joins() en rails solo acepta 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 puedo ejecutar esta consulta de la manera Rails 4?

Cualquier entrada es apreciada.

Author: Yarin, 2014-06-23

12 answers

También puede pasar una cadena que sea join-sql. eg joins("LEFT JOIN StudentEnrollment se ON c.id = se.course_id")

Aunque usaría rails - standard table naming para mayor claridad:

joins("LEFT JOIN student_enrollments ON courses.id = student_enrollments.course_id")
 69
Author: Taryn East,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-06-23 05:21:58

En realidad hay una "Manera Rails" para hacer esto.

Puedes usar Arel , que es lo que Rails usa para construir consultas para ActiveRecrods

Lo envolvería en método para que puedas llamarlo bien y pasar cualquier argumento que quieras, 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 almacenar modelos en la memoria que tal vez no necesite (como en su caso)
Consulte Rails ActiveRecord:: QueryMethods .eager_load
Hace exactamente lo que estás pidiendo de una manera ordenada.

 20
Author: superuseroi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-04 22:04:24

Si alguien vino aquí buscando una forma genérica de hacer una unión externa izquierda en Rails 5, puede usar el #left_outer_joins función.

Ejemplo de combinación múltiple:

Ruby:

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
 20
Author: Blaskovicz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-14 21:48:53

Ejecutarías 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 })
 8
Author: Joe Kennedy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-02-17 17:30:25

Combinando includes y where resulta en ActiveRecord realizando una UNIÓN EXTERNA IZQUIERDA detrás de las escenas (sin donde esto generaría el conjunto normal de dos consultas).

Así que 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

 8
Author: mackshkatz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-20 17:43:33

Agregando a la respuesta anterior, para usar includes, si desea una COMBINACIÓN EXTERNA sin hacer referencia a la tabla en el where (como id siendo nil) o la referencia está en una cadena, 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

 8
Author: Jonathon Gardner,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-24 17:58:57

Es una consulta de unión en el Modelo Activo en Rails.

Haga clic aquí para obtener más información sobre el Formato de Consulta del 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
 4
Author: jainvikram444,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-06-23 08:57:33

He estado luchando con este tipo de problema durante bastante tiempo, y decidí hacer algo para resolverlo de una vez por todas. Publiqué una síntesis que aborda este tema: https://gist.github.com/nerde/b867cd87d580e97549f2

He creado un pequeño truco de AR que usa la tabla Arel para construir dinámicamente las uniones izquierdas para ti, sin tener que escribir SQL sin procesar en tu 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 que ayude.

 4
Author: Diego,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-06-12 12:01:34

Use Squeel :

Person.joins{articles.inner}
Person.joins{articles.outer}
 3
Author: Yarin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-12 12:45:20

Si desea uniones EXTERNAS sin todos los objetos ActiveRecord extra cargados con impaciencia, use .pluck(:id) después de .eager_load() para abortar la carga ansiosa mientras conserva la UNIÓN EXTERNA. Usar .pluck(:id) frustra la carga ansiosa porque los alias de nombre de columna (items.location AS t1_r9, por ejemplo) desaparecen de la consulta generada cuando se usan (estos campos con nombre independiente se usan para crear instancias de todos los objetos ActiveRecord cargados ansiosamente).

Una desventaja de este enfoque es que luego necesita ejecutar una segunda consulta para introduzca 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)
 2
Author: textral,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-07 06:43:08

Sé que esta es una vieja pregunta y un viejo hilo, pero en Rails 5, simplemente podrías hacer

Course.left_outer_joins(:student_enrollments)
 2
Author: jDmendiola,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-30 04:16:11

Puedes usar la gema left_joins, que retroporta el método left_joins de Rails 5 para Rails 4 y 3.

Course.left_joins(:student_enrollments)
      .where('student_enrollments.id' => nil)
 1
Author: khiav reoy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-27 04:01:05