Home

Rails has_many :through Polymorphic Association

How to maintain HMT behavior on a polymorphic association.

If you're not familiar with the has_many ..., :through ... association in rails, it's a great way to add a many-to-many relationship, between two models, while storing more than just the association on the join model.

The Simple Way

Let's say you have a Post model and it has a many-to-many relationship with a Tag model. You might use a Tagging model to connect the two. A simple setup might look like this:

app/models/post.rb

class Post < ActiveRecord::Base
has_many :taggings
has_many :tags, :through => :taggings
end

app/models/tag.rb

class Tag < ActiveRecord::Base
has_many :taggings
has_many :posts, :through => :taggings
end

app/models/tagging.rb

class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :post
end

Where the taggings table would have a post_id and tag_id integer columns.

Making It Polymorphic

But what if you need to extend your Tagging model so you can tag all sorts of other models, like an Image model, for example? The best way to do that is through a polymorphic association.

In this case, you would replace your post_id column on your Tagging model with taggable_id (integer) and taggable_type (string) columns. Then, your join model would look like this:

app/models/tagging.rb

class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :taggable, :polymorphic => true
end

For the post model, we now have to know we are getting to the Tag model through the taggable polymorphic association. So, your Post model changes to:

app/models/post.rb

class Post < ActiveRecord::Base
has_many :taggings, :as => :taggable
has_many :tags, :through => :taggings
end

And let's say you wanted to make an image model taggable. That would look like this:

app/models/image.rb

class Image < ActiveRecord::Base
has_many :taggings, :as => :taggable
has_many :tags, :through => :taggings
end

That's it. That's easy. Except, for example, if you want to list all the posts from a certain tag, then you can't get there by doing this:

@tag = Tag.find_by_id(params[:id])
@posts = @tag.posts

You could do this:

@tag = Tag.find_by_id(params[:id])
@posts = @tag.taggable.where(:taggable_type => 'Post')

But that's ugly.

So, if we want to keep that @tag.posts call in tact, we have to add a specific association to the Tag model:

app/models/tag.rb

class Tag < ActiveRecord::Base
has_many :taggings
has_many :posts, :through => :taggings, :source => :taggable,
:source_type => 'Post'
end

In other words, you need to tell rails explicitly how to get to posts from tags, otherwise it gets confused.

And if you wanted to add images to the mix, you would just add this:

app/models/tag.rb

class Tag < ActiveRecord::Base
has_many :taggings
has_many :posts, :through => :taggings, :source => :taggable,
:source_type => 'Post'
has_many :images, :through => :taggings, :source => :taggable,
:source_type => 'Image'
end

Links:

Let's Connect

Keep Reading

Bi-Directional has_and_belongs_to_many on a Single Model in Rails

Bi-directional HABTM relationships are easy in Rails, but when you need to do it on a single model, that's when it gets tricky. Here's one approach.

May 04, 2015

Transition Between Database Adapters in Rails

Transitioning from one database to another, or even to a whole new database with a new adapter, can be tough. Here's an easy way to transition content.

May 25, 2015

Convert PDF to Image with Dragonfly and Rails

Converting a PDF to an image using Rails and Dragonfly is actually quite simple. Check it out.

Jan 06, 2015