How to model a many-to-many relationship in rails

Published on September 09, 2010 by Toran Billups

Any rails screencast will usually show a simple blog example to get you started. But often they skip the many-to-many relationship and jump to the less complex one-to-many implementation of comments and posts. I instead wanted to start with the seemingly more difficult many tags to many posts relationship.

Scaffold out the objects and join table

First create a new rails application using a MySQL backend

rails new blog -d mysql

Change to the directory of your new rails app

cd blog

Next we need to scaffold out the post and tag objects

rails generate scaffold Post title:string body:text
rails generate scaffold Tag description:string

But this is where it gets a little tricky. Rails won't build the many-to-many join table so you will need to generate a specific migration that will hold these primary key values

rails generate migration create_posts_tags_join

After you have this file created you need to alter it. You can find this file under db/migrate/. It Should be named something like 0000000_create_posts_tags_join.rb. It's important to note that you must put these in alpha order ( ie p comes before t )

1
2
3
4
5
6
7
8
9
10
11
12
13
class CreatePostsTagsJoin < ActiveRecord::Migration
  def self.up
    create_table 'posts_tags', :id => false do |t|
      t.column 'post_id', :integer
      t.column 'tag_id', :integer
    end
  end

  def self.down
    drop_table 'posts_tags'
  end
end
</pre>

Next in the post model add the following. This can be found under app/models

has_and_belongs_to_many :tags

And in the tag model add the following

has_and_belongs_to_many :posts

And finally do rake db:create followed by rake db:migrate from the command line to get your database setup correctly.

Alter your views to reflect this relationship

To get a working view first edit your _form.html.erb to include a checkbox of tags. This can be found under app/views/posts/

tag markup form

Next in the controller you need to add a variable for each element using the form. You can find this class under app/controllers/

1
2
3
4
5
6
7
8
9
10
def edit
  @tags = Tag.all
  ...
end

def new
  @tags = Tag.all
  ...
end
</pre>

Next alter the show.html.erb so we can see each tag for a given post - found under app/views/posts

tag markup show

Next you need to add a line to your show controller action that returns only the tags for a given post - again the posts_controller

1
2
3
4
5
6
7
8
9
def show
    @post = Post.find(params[:id])
    @tags = @post.tags

    respond_to do |format|
      format.html # show.html.erb
      format.xml  { render :xml => @post }
    end
end

And one last tweak to fix an issue with a 'no many-to-many' update. Alter your update method to show the tag_ids array as null when it's empty. This will clear the collection from @post when you submit a form post without any tags selected.

1
2
3
4
5
def update
  params[:post][:tag_ids] ||= []
  @post = Post.find(params[:id])
  ...
end

Lets see this in action. Fire up the rails web server

rails server

Now after you have the above in place go to the url below and add a few tags so we have some to play with

http://localhost:3000/tags

Next go to the post url below and add a post, but you should see a checkbox at the bottom that allows you to mark a post with a specific tag

http://localhost:3000/tags

Check a few and click update. Next edit it and remove them all. You now have your first many-to-many relationship working in rails 3

If you want the source for the finished blog application you can download it here