web-dev-qa-db-fra.com

Rails inclut avec conditions

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.

52
gusa

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)
28
Leo Correa

Syntaxe Rails 5+:

Person.includes(:notes).where(notes: {important: true})

Imbriqué:

Person.includes(notes: [:grades]).where(notes: {important: true, grades: {important: true})
20
Graham Slick

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" = ?
14
Daniel Loureiro

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

4
juliangonzalez

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.

https://ja.stackoverflow.com/q/22812/754

2
Yuki Inoue

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.

0
kirstu

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

0
Yoko