2010-06-15 13 views
8

Tengo dos modelos: Item y Tag. Ambos tienen un atributo de nombre. Quiero encontrar elementos etiquetados con varias etiquetas.rieles: Obtener todos los elementos etiquetados x AND yY z

class Item < ActiveRecord::Base 
    has_many :tags 
    validates_presence_of :name 
end 

class Tag < ActiveRecord::Base 
    belongs_to :item 
    validates_presence_of :name 
end 

Dada una lista de identificadores de etiqueta, puedo fácilmente lo suficientemente obtener la lista de artículos etiquetados con una etiqueta o la otra:

# Find the items tagged with one or more of the tags on tag_ids 
Item.all(:conditions => ['tags.id in (?)', tag_ids], :joins => :tags) 

Si tag_ids es {1,4}, entonces consigo todas las imágenes etiquetado con 1, o 4, o ambos.

quiero saber ahora cómo conseguir las imágenes que están etiquetadas con ambos - 1 Y 4.

ni siquiera puedo imaginar el SQL que se necesita aquí.

+0

Estoy teniendo el mismo problema ... ¡Qué casualidad! – fjuan

+0

Creo que el código también le da "duplicados" si un elemento también está etiquetado con "ambas" etiquetas, lo que puede no ser bueno (use: group => 'items.id' para que no devuelva duplicados). – rogerdpack

Respuesta

13

Puede resolver esto mediante la agrupación de los resultados y la comprobación de la cuenta:

Item.all(
    :conditions => ['tags.id IN (?)', tag_ids], 
    :joins  => :tags, 
    :group  => 'items.id', 
    :having  => ['COUNT(*) >= ?', tag_ids.length] 
)
+0

Esto es simple y elegante. Me ayudó mucho. ¡Gracias! – kikito

2

Tengo una cosa que añadir a elektronaut es la respuesta maravillosa de lo contrario: no va a funcionar en PostgreSQL.

En mi ejemplo real, la llamada Item.all incluye otras tablas; por lo que las miradas de selección como esta:

SELECT items.id AS t0_f0, items.name as t0_f1 ..., table2.field1 as t1_f0 .. etc 

GRUPO de PostgreSQL requiere que todos los campos utilizadas en una selección para ser incluidos allí. Así que tuve que incluir todos los campos usados ​​en la selección previa en la cláusula GROUP BY.

Y todavía no funcionó; No estoy seguro por qué.

Terminé haciendo algo más simple y feo. Requiere dos solicitudes de db. Uno de ellos se usa para devolver identificadores, que se usan como condición.

class Item < ActiveRecord::Base 

    # returns the ids of the items tagged with all tags 
    # usage: Item.tagged_all(1,2,3) 
    named_scope :tagged_all, lambda { |*args| 
    { :select => "items.id", 
     :joins => :tags, 
     :group => "items.id", 
     :having => ['COUNT(items.id) >= ?', args.length], 
     :conditions => ["tags.id IN (?)", args] 
    } 
    } 

Entonces puedo hacer esto:

Item.all(
    :conditions => [ 
     'items.id IN (?) AND ... (other conditions) ...', 
     Items.tagged_all(*tag_ids).collect(&:id), 
     ... (other values for conditions) ... 
    ], 
    :includes => [:model2, :model3] #tags isn't needed here any more 
) 

Hacky, pero funciona, y el hackyness está localizado.

3

pequeña actualización: Hoy en día, podemos utilizar (inspirado por elektronaut):

Item.joins(:tags).where("tags.label in (?)", tags).group('items.id').having("COUNT(*) >= ?", tags.size) 

No es muy diferente, por lo que funciona bien aquí.

+0

¡Esta es una gran solución! +1 (solo asegúrese de tener un índice en tags.label). –

Cuestiones relacionadas