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/
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
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
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.
Thanks! Perfect example!
fantastic
Aaahhhh! Thanks for this article, "include" thing was the clue. You saved my life.
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
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...