2012-03-08 10 views
71

Esto parece bastante simple pero no puedo conseguir que aparezca en Google.Los raíles encuentran registro con cero has_many records asociados

Si tengo:

class City < ActiveRecord::Base 
    has_many :photos 
end 

class Photo < ActiveRecord::Base 
    belongs_to :city 
end 

Quiero encontrar todas las ciudades que no tienen fotos. Me encantaría poder llamar algo como ...

City.where(photos.empty?) 

... pero eso no existe. Entonces, ¿cómo haces este tipo de consulta?


Actualización: Habiendo encontrado una respuesta a la pregunta original, tengo curiosidad, ¿cómo se construye la inversa?

IE: si quería crear estos como alcances:

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

Desde que descubrí esta pregunta (http://stackoverflow.com/q/5319400/417872) Creo que esto se puede cerrar . Sin embargo, probablemente valga la pena tener una forma más de encontrar esto en Google, este tipo de cosas es difícil de describir y, por lo tanto, es difícil de buscar. – Andrew

+11

En Rails 4, puede usar el nuevo método '.not' para el inverso. 'City.includes (: fotos) .where.not (fotos: {city_id: nil})' –

+2

Posible duplicado de [Quiere buscar registros sin registros asociados en Rails 3] (https://stackoverflow.com/questions/ 5319400/want-to-find-records-with-no-associated-records-in-rails-3) –

Respuesta

103

Bah, encontramos aquí: https://stackoverflow.com/a/5570221/417872

City.includes(:photos).where(photos: { city_id: nil }) 
+1

Vea también: http://stackoverflow.com/a/19080147/492465 - también responde su pregunta sobre la construcción de la inversa y hace todo esto con Arel – novemberkilo

+4

No entiendo cómo es esto correcto? ¿No está buscando fotos que no tienen un 'city_id'?Eso no es lo mismo que las ciudades para las cuales no hay una foto con la identificación de esa ciudad en particular como la clave externa. – sixty4bit

+4

@ sixty4bit - Funciona porque cuando haces 'includes' hace un join. En una combinación SQL, obtiene todos los campos de ambas tablas (en este caso, ciudades y fotos) para cada fila a menos que cambie la proyección de la consulta. Por lo tanto, lo está usando para su ventaja para verificar si hay un identificador de base de datos requerido. Si no es así, entonces no había ningún registro en el lado de las fotos de la unión. También podría usar 'photos: {id: nil}' si eso es más claro. –

21

Al tratar de encontrar registros sin registros correspondientes de la tabla unida, es necesario utilizar un LEFT OUTER JOIN

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') 
+0

esto es mucho más lento en comparación con la respuesta de Andrew, pero funciona – brauliobo

+0

En realidad, diría que esto es menos complicado: esta respuesta demuestra la técnica real utilizada, o la consulta DB real que se necesitará. La respuesta aceptada esencialmente ofusca esto. – Todd

+2

Si alguien más se pregunta, 'IZQUIERDA UNIÓN EXTERIOR' es equivalente a 'UNIR IZQUIERDO' – Daniel

4

que utiliza una combinación de obtener todos los con fotos:

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

más fácil de escribir y comprender, para ese caso en particular. No estoy seguro de lo que la eficiencia es de hacer una unión vs haciendo una incluye, aunque

22

En rieles 5, para encontrar todas las ciudades que no tienen fotos, puede utilizar left_outer_joins:

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

lo que se traducirá en SQL como:

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

Usando includes:

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

tendrá el mismo resultado, pero se traducirá en SQL mucho más feo como:

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 
+0

¿Cuál es la versión de Rails 4 de esto? – fatuhoku

+0

@fatuhoku En ** Rails 4 ** puede usar [la respuesta de Yossi] (https://stackoverflow.com/a/23756239/6231376) (para un SQL más limpio) o usar 'includes' (para obtener un código menos frágil). En Rails 5 'left_outer_joins' es el camino. – TeWu

Cuestiones relacionadas