2010-06-15 1 views
8

Ich habe zwei Modelle: Item und Tag. Beide haben ein Namensattribut. Ich möchte Elemente finden, die mit mehreren Tags versehen sind.Rails: Get alle Artikel markiert mit x UND y UND 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 

eine Liste von Tag-IDs gegeben, kann ich mich leicht genug, um die Liste der Einzelteile mit einem Tag oder anderen getaggt:

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

Wenn tag_ids{1,4} ist, dann bekomme ich alle Bilder getaggt mit 1, oder 4 oder beiden.

Ich will jetzt wissen, wie die Bilder zu bekommen, die mit beiden markiert sind - 1 UND 4.

ich die SQL nicht einmal vorstellen kann, die hier gebraucht wird.

+0

Ich habe das gleiche Problem ... Was für ein Zufall !! – fjuan

+0

Ich glaube, dass Code auch "Duplikate" gibt, wenn ein Gegenstand auch mit "beiden" -Tags versehen ist, was möglicherweise nicht gut ist (benutze: group => 'items.id', damit es keine Duplikate zurückgibt). – rogerdpack

Antwort

13

Sie können dieses Problem lösen, indem die Ergebnisse gruppieren und die Überprüfung der Zählung:

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

Das ist einfach und elegant. Es hat mir sehr geholfen. Vielen Dank! – kikito

2

Ich habe eine Sache bekam elektronaut ist sonst wunderbare Antwort hinzuzufügen: es wird auf PostgreSQL nicht funktionieren.

In meinem realen Beispiel enthält der Call Item.all andere Tabellen; so die Auswahl sieht wie folgt aus:

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

PostgreSQLs GROUP BY erfordert, dass alle Felder ein ausgesuchtes verwendet, um dort aufgenommen werden. Also musste ich alle Felder, die bei der vorherigen Auswahl verwendet wurden, in die GROUP BY-Klausel aufnehmen.

Und trotzdem hat es nicht funktioniert; Ich bin mir nicht sicher warum.

Ich landete eine einfachere, hässlichere Sache. Es erfordert zwei Datenbankanforderungen. Einer von ihnen wird verwendet, um IDs zurückzugeben, die als Bedingung verwendet werden.

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

Dann kann ich dies tun:

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, aber es funktioniert, und die hackyness lokalisiert ist.

3

Kleines Update: Heute können wir (inspiriert von elektronaut) verwenden:

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

Es ist nicht sehr verschieden, indem sie sich hier gut funktioniert.

+0

Dies ist eine großartige Lösung! +1 (stellen Sie sicher, dass Sie einen Index für tags.label haben). –

Verwandte Themen