Question

I'm working on a Rails 3 application that has (for the sake of this question) Posts linked to multiple Categories and vice versa through has_and_belongs_to_many associations:

Post < ActiveRecord::Base
  has_and_belongs_to_many :categories
end

Category < ActiveRecord::Base
  has_and_belongs_to_many :posts
end

I'm trying to figure out how to write an ActiveRecord (or ARel) finder that retrieves all Posts where each Post is linked to both of two Categories. I understand the SQL I'm ultimately trying to generate (two INNER JOINS with aliases to be able to distinguish each one for the matching on each of the two Categories), but so far I haven't figured out a way to create the query without resorting to raw SQL bits.

The reason avoiding custom SQL is so important in this case is that the code I'm writing is generic and heavily data-driven, and it needs to mix with other filtering (and sorting) qualifiers on the query for Post objects, so I can't just hard code either method calls on Post (e.g. to access the collection of Categories) or custom SQL that might not mix well with the SQL generated by the other filters.

I'm open to switching to using a join model (has_many :through) if that somehow makes things easier, or even looking at other ORM options (DataMapper, Mongoid, etc.), but that seems like a huge change just to get something so basic working.

I'm stunned that this isn't easier/more obvious in ActiveRecord/ARel, but maybe I just don't know the magic keywords to search to find the answer. The documentation for ARel is also surprisingly slim, so I'm at a loss. Any help would be much appreciated!

Was it helpful?

Solution

After quite a bit more Googling, I found these two articles (from 2006!) that eventually lead me to the correct answer in ActiveRecord/ARel:

http://blog.hasmanythrough.com/2006/6/12/when-associations-arent-enough

http://blog.hasmanythrough.com/2006/6/12/when-associations-arent-enough-part-2

The (nearly) SQL-free code that produces what I'm looking for is a pretty clever use of the GROUP BY and HAVING operators in SQL:

Post.joins(:categories).where("categories.name" => ['catA','catB']).group('posts.id').having('COUNT(posts.id) = 2')

Basically, it finds all Posts associated with any of the given Categories (including duplicates if, as we hope, there are Posts that match multiple Categories), groups that list by the id field on Post, then trims the results down to only include groups that have exactly the number of matches we want.

I haven't tried mixing this code with my filters on other fields on Post, but I'm pretty sure it'll work.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top