web-dev-qa-db-fra.com

Rails find record with zero has_many records associated

Cela semble assez simple mais je n'arrive pas à le faire apparaître sur Google.

Si j'ai:

class City < ActiveRecord::Base
  has_many :photos
end

class Photo < ActiveRecord::Base
  belongs_to :city
end

Je veux trouver toutes les villes qui n'ont pas de photos. J'adorerais pouvoir appeler quelque chose comme ...

City.where( photos.empty? )

... mais cela n'existe pas. Alors, comment faites-vous ce genre de requête?


pdate: Ayant maintenant trouvé une réponse à la question d'origine, je suis curieux, comment construisez-vous l'inverse?

IE: si je voulais les créer comme portées:

scope :without_photos, includes(:photos).where( :photos => {:city_id=>nil} )
scope :with_photos, ???
87
Andrew

Bah, je l'ai trouvé ici: https://stackoverflow.com/a/5570221/417872

City.includes(:photos).where(photos: { city_id: nil })
121
Andrew

Dans Rails 5 , pour trouver toutes les villes qui n'ont pas de photos, vous pouvez utiliser left_outer_joins :

City.left_outer_joins(:photos).where(photos: {id: nil})

qui se traduira par SQL comme:

SELECT cities.*
FROM cities LEFT OUTER JOIN photos ON photos.city_id = city.id
WHERE photos.id IS NULL

En utilisant includes :

City.includes(:photos).where(photos: {id: nil})

aura le même résultat, mais entraînera un SQL beaucoup plus laid comme:

SELECT cities.id AS t0_r0, cities.attr1 AS t0_r1, cities.attr2 AS t0_r2, cities.created_at AS t0_r3, cities.updated_at AS t0_r4, photos.id AS t1_r0, photos.city_id AS t1_r1, photos.attr1 AS t1_r2, photos.attr2 AS t1_r3, photos.created_at AS t1_r4, photos.updated_at AS t1_r5
FROM cities LEFT OUTER JOIN photos ON photos.city_id = cities.id
WHERE photos.id IS NULL
45
TeWu

Lorsque vous essayez de trouver des enregistrements sans enregistrements correspondants dans la table jointe, vous devez utiliser une jointure externe gauche

scope :with_photos, joins('LEFT OUTER JOIN photos ON cities.id = photos.city_id').group('cities.id').having('count(photos.id) > 0')
scope :without_photos, joins('LEFT OUTER JOIN photos ON cities.id = photos.city_id').group('cities.id').having('count(photos.id) = 0')
22
Yossi Shasho

J'ai utilisé une jointure pour obtenir toutes celles avec photos:

scope :with_photos, -> { joins(:photos).distinct }

Plus facile à écrire et à comprendre, pour ce cas particulier. Je ne suis pas sûr de l'efficacité d'une jointure vs d'une inclusion, bien que

6
Onikoroshi

Si vous n'exécutez pas Rails 5+ et que les performances sont indispensables, évitez la création inutile d'ActiveRecord et obtenez exactement ce dont vous avez besoin:

City.where("NOT EXISTS(SELECT 1 FROM photos WHERE photos.city_id = cities.id LIMIT 1)")
0
RaphaMex

Je ne crois pas que la réponse acceptée vous donne exactement ce que vous recherchez, car vous voulez faire un LEFT OUTER JOIN et cette réponse vous donnera un INNER JOIN. Au moins en Rails 5 vous pouvez utiliser:

scope :without_photos, left_joins(:photos).where( photos: {id: nil} )

ou vous pouvez utiliser merge dans les cas où l'espace de noms rendra la clause where lourde:

scope :without_photos, left_joins(:photos).merge( Photos.where(id: nil) )
0
skepticscript