finding Post associated with both of two Categories in Rails 3 without custom SQL
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!
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.