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

Published 9/9/2010 8:15 PM by Toran Billups 6 Comments

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 )

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

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/

def edit
  @tags = Tag.all
  ...
end

def new
  @tags = Tag.all
  ...
end

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

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.

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

Tags: ruby rails

 

6 Comments on "How to model a many-to-many relationship in rails"

  • ryan said on 12/30/2010 1:38 PM:

    Thanks, great example. I wasn't able to get the many-to-many to work myself. Turns out the problem was I that when I created the join table, I used the label syntax for the tables and fields (ie, t.references :posts, :tags) instead of the string literal. That didn't work because the query rails used had the plural form of the column names, which obviously doesn't work. It's kind of weird that "convention over configuration" doesn't apply to many-to-many relationships.

  • v said on 3/1/2011 10:40 AM:

    Thanks! Perfect example!

  • bhavesh said on 3/1/2011 10:27 PM:

    fantastic

  • Mateusz said on 7/23/2011 6:06 PM:

    Aaahhhh! Thanks for this article, "include" thing was the clue. You saved my life.

  • Andrei Barsan said on 9/25/2011 11:20 AM:

    Gah, coming from a CodeIgniter background everything was really, really, really confusing until I cam across this awesome tutorial. :D You're awesome, man! Ah, and any newcomers reading this, remember to edit your config/routes.rb file by un-commenting the root :to line and setting the value to "posts#index", after deleting the public/index.html file. Peace and happy coding! *runs off to get some sleep and code a neat task management Rails app the next day* :3

  • syed tabrez said on 3/7/2012 1:23 AM:

    hello sir,thanks for blog app..I want to mention something that may be you have forgot to add index in join table,coz it will not work with out that..I tried a lot but end up using index...

Leave a comment

Copyright © 2009 Toran Billups - Valid XHTML & CSS