Question

I got a typical tag and whatever-object relation: say

class Tag < ActiveRecord::Base
   attr_accessible :name
   has_many :tagazations
   has_many :projects, :through => :tagazations
end

class Tagazation < ActiveRecord::Base
  belongs_to :project
  belongs_to :tag
  validates :tag_id, :uniqueness => { :scope => :project_id }
end

class Project < ActiveRecord::Base
   has_many :tagazations
   has_many :tags, :through => :tagazations
end

nothing special here: each project is tagged by one or multiple tags.
The app has a feature of search: you can select the certain tags and my app should show you all projects which tagged with ALL mentioned tags. So I got an array of the necessary tag_ids and then got stuck with such easy problem

Était-ce utile?

La solution

To do this in one query you'd want to take advantage of the common double not exists SQL query, which essentially does find X for all Y.

In your instance, you might do:

class Project < ActiveRecord::Base
  def with_tags(tag_ids)
    where("NOT EXISTS (SELECT * FROM tags
      WHERE NOT EXISTS (SELECT * FROM tagazations
        WHERE tagazations.tag_id = tags.id
        AND tagazations.project_id = projects.id)
      AND tags.id IN (?))", tag_ids)
  end
end

Alternatively, you can use count, group and having, although I suspect the first version is quicker but feel free to benchmark:

def with_tags(tag_ids)
  joins(:tags).select('projects.*, count(tags.id) as tag_count')
    .where(tags: { id: tag_ids }).group('projects.id')
    .having('tag_count = ?', tag_ids.size)
end

Autres conseils

This would be one way of doing it, although by no means the most efficient:

class Project < ActiveRecord::Base
   has_many :tagazations
   has_many :tags, :through => :tagazations

   def find_with_all_tags(tag_names)
     # First find the tags and join with their respective projects
     matching_tags = Tag.includes(:projects).where(:name => tag_names)
     # Find the intersection of these lists, using the ruby array intersection operator &
     matching_tags.reduce([]) {|result, tag| result & tag.projects}
   end
end

There may be a couple of typos in there, but you get the idea

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top