Simulate Has Many Through HABTM

October 06, 2009 — Code

If you work much with Rails and ActiveRecord you may have noticed that a certain commonly-used association is not allowed. I will show you how to simulate it. Let’s say you have the following models and relationships:


class Article < ActiveRecord::Base
  has_and_belongs_to_many :categories
end

class Category < ActiveRecord::Base
  belongs_to :section
end

class Section < ActiveRecord::Base
  has_many :categories
end

Now you want to find all the articles in a section, so you try the obvious solution, adding the following to Section:


has_many :articles, :though => :categories

This relationship seems reasonable enough, but an exception will be raised:


ActiveRecord::HasManyThroughSourceAssociationMacroError:
Invalid source reflection macro :has_and_belongs_to_many
for has_many :articles, :through => :categories.
Use :source to specify the source reflection.

Don’t waste time trying to find the right :source. The has_many association through a has_and_belongs_to_many is simply not allowed in Rails 2.3.4, but you can get the behavior you want by defining a Section#articles method that returns a nice ActiveRecord::NamedScope proxy. Just add this named scope to your Article class:


named_scope :in_section, lambda{ |section|
  {
    :joins      => :categories,
    :conditions => {:categories => {:id => section.category_ids}},
    :select     => "DISTINCT `articles`.*" # kill duplicates
  }
}

and add this method to your Section class:


def articles
  Article.in_section(self)
end

Now, since this is a pure named scope, you can do things like section.articles.published.recent. This technique also works in situations where you’re stopped by a polymorphic association. If you find yourself using this technique a lot you might want to abstract it to a plugin. Open source, please!


comments powered by Disqus