2010-06-15 3 views
8

J'ai deux modèles: Item et Tag. Les deux ont un attribut de nom. Je veux trouver des articles marqués avec plusieurs tags.rails: Obtenez tous les éléments étiquetés x ET y ET 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 

Compte tenu de la liste des étiquettes d'identification, je peux assez facilement obtenir la liste des éléments marqués avec une étiquette ou l'autre:

# 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 est {1,4}, je reçois toutes les images étiqueté avec 1, ou 4, ou les deux.

Je veux savoir maintenant comment obtenir les photos qui sont marqués à la fois - 1 ET 4.

Je ne peux même pas imaginer le SQL qui est nécessaire ici.

+0

J'ai le même problème ... Quelle coïncidence !! – fjuan

+0

Je crois que le code vous donne aussi des "doublons" si un item est étiqueté avec les tags "both", ce qui peut ne pas être bon (use: group => 'items.id' pour qu'il ne renvoie pas de doublons). – rogerdpack

Répondre

13

Vous pouvez résoudre ce problème en regroupant les résultats et la vérification du décompte:

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

C'est simple et élégant. Cela m'a beaucoup aidé. Je vous remercie! – kikito

2

J'ai une chose à ajouter à elektronaut est autrement merveilleuse réponse: il ne fonctionnera pas sur PostgreSQL.

Sur mon exemple réel, l'appel Item.all inclut d'autres tables; de sorte que les regards de sélection comme celui-ci:

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

GROUP BY PostgreSQL exige que tous les champs utilisés sur une sélection pour y être inclus. J'ai donc dû inclure tous les champs utilisés sur le select précédent sur la clause GROUP BY.

Et encore cela n'a pas fonctionné; Je ne suis pas sûr pourquoi. J'ai fini par faire une chose plus simple et plus laide. Cela nécessite deux requêtes db. L'un d'eux est utilisé pour renvoyer des identifiants, qui sont utilisés comme condition.

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] 
    } 
    } 

Je peux le faire:

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, mais il fonctionne, et le hackyness est localisé.

3

Petite mise à jour: Aujourd'hui, nous pouvons utiliser (inspiré par elektronaut):

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

Il est pas très différente, par cela fonctionne bien ici.

+0

Ceci est une excellente solution! +1 (assurez-vous juste que vous avez un index sur tags.label). –

Questions connexes