web-dev-qa-db-fra.com

LEFT OUTER JOIN dans Rails 4

J'ai 3 modèles:

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

Je souhaite interroger une liste de cours dans la table Courses, qui n'existent pas dans la table StudentEnrollments et qui sont associés à un étudiant donné.

J'ai trouvé que Left Left est peut-être le chemin à parcourir, mais il semble que join () dans Rails n'accepte qu'un tableau comme argument. La requête SQL qui, selon moi, ferait ce que je veux est la suivante:

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

Comment exécuter cette requête de la manière Rails 4?

Toute entrée est appréciée.

74
Khanetor

Vous pouvez également transmettre une chaîne qui est join-sql. par exemple joins("LEFT JOIN StudentEnrollment se ON c.id = se.course_id")

Bien que j'utilise la dénomination de table standard Rails pour plus de clarté:

joins("LEFT JOIN student_enrollments ON courses.id = student_enrollments.course_id")
80
Taryn East

Si quelqu'un est venu ici à la recherche d'un moyen générique de faire une jointure externe gauche dans Rails 5, vous pouvez utiliser le #left_outer_joins fonction.

Exemple de multi-jointure:

Rubis:

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
25
Blaskovicz

Il y a en fait un "moyen Rails" pour le faire.

Vous pouvez utiliser Arel , qui est ce que Rails utilise pour construire des requêtes pour ActiveRecrods

Je voudrais l'envelopper dans la méthode afin que vous puissiez l'appeler correctement et transmettre l'argument que vous souhaitez, quelque chose comme:

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

Il y a aussi le moyen rapide (et légèrement sale) que beaucoup utilisent

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

eager_load fonctionne très bien, il a juste "l'effet secondaire" de stocker des modèles en mémoire dont vous n'avez peut-être pas besoin (comme dans votre cas)
Voir Rails ActiveRecord :: QueryMethods . Eager_load
Il fait exactement ce que vous demandez.

20
superuseroi

En combinant includes et where, ActiveRecord exécute un LEFT OUTER JOIN en coulisse (sans le où cela générerait le jeu normal de deux requêtes).

Pour que vous puissiez faire quelque chose comme:

Course.includes(:student_enrollments).where(student_enrollments: { course_id: nil })

Docs here: http://guides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-eager-loaded-associations

11
mackshkatz

En ajoutant à la réponse ci-dessus, pour utiliser includes, si vous voulez une jointure externe sans référencer la table dans où (comme id étant nul) ou si la référence est dans une chaîne, vous pouvez utiliser references . Cela ressemblerait à ceci:

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

ou

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

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

10
Jonathon Gardner

Vous exécuteriez la requête en tant que:

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
Joe Kennedy

Je sais que c’est une vieille question et un vieux fil mais dans Rails 5, vous pouvez simplement faire

Course.left_outer_joins(:student_enrollments)
6
jDmendiola

Vous pouvez utiliser left_joins gem, qui rétroporte left_joins méthode de Rails 5 pour Rails 4 et 3.

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

Je suis aux prises avec ce genre de problème depuis un certain temps et j'ai décidé de faire quelque chose pour le résoudre une fois pour toutes. J'ai publié un résumé qui aborde ce problème: https://Gist.github.com/nerde/b867cd87d580e97549f2

J'ai créé un petit hack AR qui utilise Arel Table pour construire dynamiquement les jointures de gauche pour vous, sans avoir à écrire du code SQL brut dans votre code:

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

J'espère que ça aide.

4
Diego

C’est une requête de jointure dans Active Model in Rails.

Cliquez ici pour plus d'informations sur le format de requête de modèle actif .

@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
jainvikram444

Utilisez Squeel :

Person.joins{articles.inner}
Person.joins{articles.outer}
3
Yarin

Si vous voulez des jointures externes sans tous les objets ActiveRecord chargés avec impatience, utilisez .pluck(:id) après .eager_load() pour annuler la charge désirée tout en préservant la jointure externe. Utiliser .pluck(:id) empêche le chargement car les alias de nom de colonne (items.location AS t1_r9, par exemple) disparaissent de la requête générée lors de leur utilisation (ces champs nommés indépendamment servent à instancier tous les objets ActiveRecord chargés avec impatience).

Un inconvénient de cette approche est que vous devez ensuite exécuter une seconde requête pour extraire les objets ActiveRecord souhaités identifiés dans la première requête:

# 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)
3
textral