A slightly advanced association in ActiveRecord is called polymorphic association. This usually happens when a model can belong to more than one model, even if it behaves the same way, but it’s easier to understand with an example.
Suppose you have a blog that has articles, photos, and events. You want to add comments on all of these, but the comments will all belong to a different kind of model (article/photo/event). So let’s start.
1. Set up/generate the comments model.
class CreateComments < ActiveRecord::Migration def change create_table :comments do |t| t.string :body t.integer :commentable_id t.string :commentable_type t.timestamps end end end
One this here to note. The commentable_id and commentable_type will store information about the article/photo/event. The word “commentable” can literally be anything you want.
2. Set up the associations with the models.
You will need to set up the associations in ALL the models that will be related to this association.
The Comment model should have the belongs_to association and the polymorphic flag checked as true:
class Comment < ActiveRecord::Base belongs_to :commentable, polymorphic: true end
And now the rest of the models should have the has_many associations:
class Article < ActiveRecord::Base has_many :comments, as: :commentable end class Event < ActiveRecord::Base has_many :comments, as: :commentable end class Photo < ActiveRecord::Base has_many :comments, as: :commentable end
This will let you retrieve comments with @photo.comments, @article.comments, @event.comments.
3. Make sure the routes.rb file is configured properly.
resources :articles do resources :comments end resources :photos do resources :comments end resources :events do resources :comments end
You essentially need a nested route of the Comment class inside each class in your app. This way your routes will look like this:
(and so forth)
4. Set up the Comments controller
In the Comment controller, do the following:
Set up a private method to find the commentable:
def find_commentable params.each do |key,value| if key =~ /(.+)_id$/ return $1.classify.constantize.find(value)
What we’re doing here is:
a. Params come in a hash, therefore we’re iterating through key-value pairs
b. If the key matches the regex /(.+)_id$/ (ex. “article_id”):
c.1 $1 is the value stored in the regex above (putting a parenthesis makes a match group)
c.2 .classify creates a class NAME, however, in a string – “article” is now “Article”
c.3 .constantize will convert it to an actual class.
Therefore: Article.find(4) -> since value is the actual value of the key-hash pair.
Now all we need is to put a before_filter in the controller:
As for the rest of the actions:
def index @comments = @commentable.comments end def new @comment = @commentable.comments.new end def create @comment = @commentable.comments.new(params[:comment]) if @comment.save redirect_to @commentable, notice: "Comment created." else render :new end end
5. Set up the view for the Comment form.
_form.html.erb partial would start off like this:
<%= form_for [@commentable, @comment] do |f| %> <%= f.text_area :content %> <%= f.submit %> <% end %>
and the link_to for a new comment would have the following params:
<%= link_to "New Comment", [:new, @commentable, :comment] %>
6. Set up the “show” action for the rest of the controllers
All you need here now is to know how things relate to each other. For example, in our Article controller:
def show @article = Article.find(params[:id]) @commentable = @article @comments = @commentable.comments @comment = Comment.new end
And then you do the rest for the other models, following the same logic.
Remember to play around in the console! It’s fun and it’ll make things clearer. Especially to understand a method like the one above, do this:
article = Article.find(2).comments
and you’ll get to see how the magic works!