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.
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")
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
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.
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 })
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
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 })
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)
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)
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.
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
Utilisez Squeel :
Person.joins{articles.inner}
Person.joins{articles.outer}
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)