Est-il possible dans Rails> 3.2 d'ajouter des conditions à l'instruction join générée par la méthode includes
?
Disons que j'ai deux modèles, Person et Note. Chaque personne a plusieurs notes et chaque note appartient à une seule personne. Chaque note a un attribut important
.
Je veux que toutes les personnes ne préchargent que les notes importantes. En SQL, ce sera:
SELECT *
FROM people
LEFT JOIN notes ON notes.person_id = people.id AND notes.important = 't'
Dans Rails, la seule façon similaire de le faire est d'utiliser includes
(note: joins
ne préchargera pas les notes) comme ceci:
Person.includes(:notes).where(:important, true)
Cependant, cela générera la requête SQL suivante qui renvoie un jeu de résultats différent:
SELECT *
FROM people
LEFT JOIN notes ON notes.person_id = people.id
WHERE notes.important = 't'
Veuillez noter que le premier jeu de résultats inclut toutes les personnes et le second uniquement les personnes associées aux notes importantes.
Notez également que: les conditions sont dépréciées depuis la 3.1.
Selon ce guide Active Record Querying
Vous pouvez spécifier des conditions sur les inclusions pour un chargement rapide comme celui-ci
Person.includes(:notes).where("notes.important", true)
Il recommande quand même d'utiliser joins
.
Une solution de contournement serait de créer une autre association comme celle-ci
class Person < ActiveRecord::Base
has_many :important_notes, :class_name => 'Note',
:conditions => ['important = ?', true]
end
Vous seriez alors en mesure de le faire
Person.find(:all, include: :important_notes)
Syntaxe Rails 5+:
Person.includes(:notes).where(notes: {important: true})
Imbriqué:
Person.includes(notes: [:grades]).where(notes: {important: true, grades: {important: true})
Rails 4.2+:
Option A - "précharge" - sélections multiples, utilise "id IN (...)")
class Person < ActiveRecord::Base
has_many :notes
has_many :important_notes, -> { where(important: true) }, class_name: "Note"
end
Person.preload(:important_notes)
SQL:
SELECT "people".* FROM "people"
SELECT "notes".* FROM "notes" WHERE "notes"."important" = ? AND "notes"."person_id" IN (1, 2)
Option B - "eager_load" - un énorme choix, utilise "LEFT JOIN")
class Person < ActiveRecord::Base
has_many :notes
has_many :important_notes, -> { where(important: true) }, class_name: "Note"
end
Person.eager_load(:important_notes)
SQL:
SELECT "people"."id" AS t0_r0, "people"."name" AS t0_r1, "people"."created_at" AS t0_r2, "people"."updated_at" AS t0_r3, "notes"."id" AS t1_r0, "notes"."person_id" AS t1_r1, "notes"."important" AS t1_r2
FROM "people"
LEFT OUTER JOIN "notes" ON "notes"."person_id" = "people"."id" AND "notes"."important" = ?
Je n'ai pas pu utiliser les inclusions avec une condition comme la réponse de Leo Correa. Insted je dois utiliser:
Lead.includes(:contacts).where("contacts.primary" =>true).first
ou vous pouvez aussi
Lead.includes(:contacts).where("contacts.primary" =>true).find(8877)
Ce dernier récupérera le Lead avec l'ID 8877 mais inclura uniquement son contact principal
La même chose a été discutée dans le stackoverflow japonais. Assez hacky, mais la suite semble fonctionner, au moins sur Rails 5.
Person.eager_load(:notes).joins("AND notes.important = 't'")
Un aspect important est que de cette façon, vous pouvez écrire une condition de jointure arbitraire. L'inconvénient est que vous ne pouvez pas utiliser d'espace réservé, vous devez donc être prudent lorsque vous utilisez des paramètres comme condition de jointure.
Une façon consiste à écrire la clause LEFT JOIN vous-même en utilisant les jointures:
Person.joins('LEFT JOIN "notes" ON "notes"."person_id" = "people.id" AND "notes"."important" IS "t"')
Pas joli, cependant.
Pour les personnes intéressées, j'ai essayé ceci où un attribut d'enregistrement était faux
Lead.includes(:contacts).where("contacts.primary" => false).first
et cela ne fonctionne pas. D'une manière ou d'une autre pour les booléens, seulement true
fonctionne, donc je l'ai retourné pour inclure where.not
Lead.includes(:contacts).where.not("contacts.primary" => true).first
Cela fonctionne parfaitement