
Be thee warned, this is a *long* article. However, it covers a lot of ground, is written in plain english, and comes with tons of code samples to make the subject matter as easy to digest as possible.
Routing has changed in Rails 3. It’s very different from 2.x, but it’s not actually difficult. This is one of those things where new users are almost better off, because they’re learning the simple way right off the bat, with no baggage. Whether you’re new to all of Rails or just Rails 3, this will get you up and running.
Static-y routes
Starting off with a fresh rails app, let’s generate our main static controller:
rails3_routing$ rails g controller home index about
create app/controllers/home_controller.rb
route get "home/about"
route get "home/index"
invoke erb
create app/views/home
create app/views/home/index.html.erb
create app/views/home/about.html.erb
invoke test_unit
create test/functional/home_controller_test.rb
invoke helper
create app/helpers/home_helper.rb
invoke test_unit
create test/unit/helpers/home_helper_test.rb
We can clean up our routes file so it looks something like this:
Rails3Routing::Application.routes.draw do
get "home/index"
get "home/about"
root :to => "home#index"
end
The controller generator created the two static routes for us, and I made home/index the root (default) route for the application. Now I need to get rid of the default index file in the public directory:
rails3_routing$ ls public
404.html 500.html images javascripts stylesheets
422.html favicon.ico index.html robots.txt
rails3_routing$ rm public/index.html
rails3_routing$ ls public
404.html 500.html images robots.txt
422.html favicon.ico javascripts stylesheets
If we don’t remove this, the app will show the public/index.html file as the home page. So far so good! If you fire up the server, you’ll see our home/index action is being called as our home page.
RESTful Routes
Let’s use the example of a blog application, and create the Post model, views, and controller to look at RESTful routing in Rails 3. We’ll use the scaffold generator to create the entire MVC (Model-View-Controller) stack in one shot:
rails3_routing$ rails g scaffold post title:string content:text
invoke active_record
create db/migrate/20101202051750_create_posts.rb
create app/models/post.rb
invoke test_unit
create test/unit/post_test.rb
create test/fixtures/posts.yml
route resources :posts
invoke scaffold_controller
create app/controllers/posts_controller.rb
invoke erb
create app/views/posts
create app/views/posts/index.html.erb
create app/views/posts/edit.html.erb
create app/views/posts/show.html.erb
create app/views/posts/new.html.erb
create app/views/posts/_form.html.erb
invoke test_unit
create test/functional/posts_controller_test.rb
invoke helper
create app/helpers/posts_helper.rb
invoke test_unit
create test/unit/helpers/posts_helper_test.rb
invoke stylesheets
create public/stylesheets/scaffold.css
Looking at our routes.rb file, you can see we now have our first RESTful route, added automatically by Rails:
Rails3Routing::Application.routes.draw do
resources :posts
get "home/index"
get "home/about"
root :to => "home#index"
end
This doesn’t look like much – it only added a line to our routes file – but it packs a punch. For starters, it’s defining the seven basic RESTful routes:
- GET index
- GET show
- GET new
- POST create
- GET edit
- PUT update
- DELETE destroy
These are the seven routes needed to list, view, add, update, and remove posts in our application. But what if we want to add custom routes to this controller? Maybe we want a couple actions called recent that shows only the latest 5 posts, and popular that shows the top 5 popular posts. Let’s also add an action called publish that will activate a post once it’s ready, so people can see it. Let’s add these to our routes:
resources :posts do
get 'recent', :on => :collection
get 'popular', :on => :collection
put 'publish', :on => :member
end
Both recent and popular use http GET requests, because we’re not changing any data on the server. The publish action uses the http PUT request, to denote a route that does changes records on the server.
The publish action is a member action, meaning when we call this action, the route will have to contain the id of the record we want to work with, like this: “/posts/1/publish”. The other two actions are collection-oriented, meaning they work with the collection as a whole and we don’t need to pass a specific id to them. The recent route would look like “/posts/recent”.
When you start adding routes, the notation above may get tedious. Luckily, there’s a shortcut:
resources :posts do
collection do
get 'recent'
get 'popular'
end
member do
put 'publish'
end
end
This is the preferred way to notate custom routes, whether you have many or just a couple. It’s easier to read, and easier to update with more routes in the future.
Nested Routes
Now let’s say we want to add comments to our application. Obviously every comment will be attached to a specific post, and we’d like the routes to reflect this. It’s not absolutely necessary, but it does add much needed structure in these trying and uncertain times. Let’s generate our comments:
rails3_routing$ rails g scaffold comment title:string content:text post_id:integer
invoke active_record
create db/migrate/20101202054608_create_comments.rb
create app/models/comment.rb
invoke test_unit
create test/unit/comment_test.rb
create test/fixtures/comments.yml
route resources :comments
invoke scaffold_controller
create app/controllers/comments_controller.rb
invoke erb
create app/views/comments
create app/views/comments/index.html.erb
create app/views/comments/edit.html.erb
create app/views/comments/show.html.erb
create app/views/comments/new.html.erb
create app/views/comments/_form.html.erb
invoke test_unit
create test/functional/comments_controller_test.rb
invoke helper
create app/helpers/comments_helper.rb
invoke test_unit
create test/unit/helpers/comments_helper_test.rb
invoke stylesheets
identical public/stylesheets/scaffold.css
Now our routing file will have our comments routes, but they’re not nested inside our posts routes. Let’s fix that:
resources :posts do
resources :comments
collection do
get 'recent'
get 'popular'
end
member do
put 'publish'
end
end
Easy! We just moved the resource line into our posts block. In order to complete the process, I’ve added the necessary relationships in the models, before filter in the comments controller (to lookup the parent post) and updated the comments controller to perform all comment lookups from the perspective of the parent post. Feel free to view the source code for this article (link at the top of the page) to see how I did this.
Now if you’re editing a comment with id 100 that belongs to post 20, the route will look like: “/posts/20/comments/100/edit”.
Namespaced Routes
After a while, you might realize that your posts and comments need to be treated differently depending on the context. One way to do this is with namespacing. For example, you might want to create an admin section of the site where posts are shown with more options (re-ordering, editing, etc). While I won’t get into the permissions issues in this article, this is easy to do from a routing perspective.
First, we’ll create the namespaced controller:
rails3_routing$ rails g controller admin/posts index show new create edit update destroy
create app/controllers/admin/posts_controller.rb
route get "posts/destroy"
route get "posts/update"
route get "posts/edit"
route get "posts/create"
route get "posts/new"
route get "posts/show"
route get "posts/index"
invoke erb
create app/views/admin/posts
create app/views/admin/posts/index.html.erb
create app/views/admin/posts/show.html.erb
create app/views/admin/posts/new.html.erb
create app/views/admin/posts/create.html.erb
create app/views/admin/posts/edit.html.erb
create app/views/admin/posts/update.html.erb
create app/views/admin/posts/destroy.html.erb
invoke test_unit
create test/functional/admin/posts_controller_test.rb
invoke helper
create app/helpers/admin/posts_helper.rb
invoke test_unit
create test/unit/helpers/admin/posts_helper_test.rb
As you can see, calling the controller “admin/posts” caused all the newly created files to be put in their own admin directory. If we look in the routes file, we’ll see this ugliness:
get "posts/index"
get "posts/show"
get "posts/new"
get "posts/create"
get "posts/edit"
get "posts/update"
get "posts/destroy"
The controller generator doesn’t know that we want RESTful, namespaced routes, so we’ll drop all those lines in favor of this:
namespace :admin do
resources :posts
end
Note that we’re adding posts resources again, but this time they’re namespaced. They won’t interfere with our regular posts routes, because these routes will be prefixed with “/admin”. So editing the post with id “100″ in the admin section will have a route like this: “/admin/posts/100/edit”.
Things You Should Know
While our initial routes file is pretty simple and straightforward, it would quickly grow if this were a real app. How does the Rails 3 routing engine choose from conflicting or overlapping routes? It does this the same way Rails 2.x does – it works top to bottom. It does not try to find the *best* match (the way a google search might) – instead, it starts at the top of the routes file and stops when it finds the first acceptable match. Therefore, default routes should be pushed to the bottom.
While we’re on the subject, don’t use default routes. Named routes form a much more secure and stable application. There shouldn’t be routes you’re NOT expecting. One exception can be short urls. If you want members to have a short url the way Twitter does (http://twitter.com/kconrails for instance) then you’ll need a default route, placed at the bottom of the routes file, to be tried after all other routes have been checked. If you had a members controller, and the member name should route to the show action, the route might look like this:
match ':member' => 'members#show', :as => :shorturl
Finally, when in doubt, check your existing routes with this rake task:
rails3_routing$ rake routes
(in /Users/bellmyer/Desktop/bellmyer/blog/rails3_routing)
admin_posts GET /admin/posts(.:format) {:action=>"index", :controller=>"admin/posts"}
admin_posts POST /admin/posts(.:format) {:action=>"create", :controller=>"admin/posts"}
new_admin_post GET /admin/posts/new(.:format) {:action=>"new", :controller=>"admin/posts"}
edit_admin_post GET /admin/posts/:id/edit(.:format) {:action=>"edit", :controller=>"admin/posts"}
admin_post GET /admin/posts/:id(.:format) {:action=>"show", :controller=>"admin/posts"}
admin_post PUT /admin/posts/:id(.:format) {:action=>"update", :controller=>"admin/posts"}
admin_post DELETE /admin/posts/:id(.:format) {:action=>"destroy", :controller=>"admin/posts"}
post_comments GET /posts/:post_id/comments(.:format) {:action=>"index", :controller=>"comments"}
post_comments POST /posts/:post_id/comments(.:format) {:action=>"create", :controller=>"comments"}
new_post_comment GET /posts/:post_id/comments/new(.:format) {:action=>"new", :controller=>"comments"}
edit_post_comment GET /posts/:post_id/comments/:id/edit(.:format) {:action=>"edit", :controller=>"comments"}
post_comment GET /posts/:post_id/comments/:id(.:format) {:action=>"show", :controller=>"comments"}
post_comment PUT /posts/:post_id/comments/:id(.:format) {:action=>"update", :controller=>"comments"}
post_comment DELETE /posts/:post_id/comments/:id(.:format) {:action=>"destroy", :controller=>"comments"}
recent_posts GET /posts/recent(.:format) {:action=>"recent", :controller=>"posts"}
popular_posts GET /posts/popular(.:format) {:action=>"popular", :controller=>"posts"}
publish_post PUT /posts/:id/publish(.:format) {:action=>"publish", :controller=>"posts"}
posts GET /posts(.:format) {:action=>"index", :controller=>"posts"}
posts POST /posts(.:format) {:action=>"create", :controller=>"posts"}
new_post GET /posts/new(.:format) {:action=>"new", :controller=>"posts"}
edit_post GET /posts/:id/edit(.:format) {:action=>"edit", :controller=>"posts"}
post GET /posts/:id(.:format) {:action=>"show", :controller=>"posts"}
post PUT /posts/:id(.:format) {:action=>"update", :controller=>"posts"}
post DELETE /posts/:id(.:format) {:action=>"destroy", :controller=>"posts"}
home_index GET /home/index(.:format) {:controller=>"home", :action=>"index"}
home_about GET /home/about(.:format) {:controller=>"home", :action=>"about"}
root /(.:format) {:controller=>"home", :action=>"index"}
It will give you a list of all available routes that have been setup in your application. It also gives you the route names. The first word of each line is the route name. The first named route in our example is “admin_posts”. You can append “_url” or “_path” in views and controllers to get either that action’s url with or without the hostname, respectively.
Also note that in the routes listed above, any symbols that are NOT in parentheses are required. For example, on line 10 above, post_comments_path would require post_id to be passed in. You can do this with either the id, or the object itself. For example
post_comments_path(@post)
post_comments_path(@post.id)
This is a lot to play with, which is why I like including code samples with articles like this. Download (link at the top of the page) and play around with the routes until you're comfortable. After and hour or so, you won't need to lookup the Rails specs every time you want to make a routing change.
Nested Comments in Ruby on Rails, Part 1: Models
October 23, 2010YouTube has a pretty cool comment system. You can comment on videos, but you can also reply to comments other people have posted. In essence, you commenting on comments!
If you’d like something similar in your app, you might be tempted to create PostComments and CommentComments, or something similar. A better approach is to use polymorphic associations. Polymorphism makes it possible for a comment to belong to a post, or another comment, or any number of things. And it’s easier than you think.
Note: all examples will be in Rails 3. If you’re not familiar, that’s okay. The example code is available from github, in my examples of nested comments.
Let’s start by creating a Post model:
We’re keeping it simple, and we’re not going to bother with user authentication for these examples. Our post model is pretty straightforward, so we didn’t need to modify the migration file at all. Now let’s create our comments:
This is where some of the magic comes in. We can’t say our comments “belong to posts” because they can belong to anything. So we can’t add a “post_id” field. Instead, we come up with a name for our association: “commentable”. We add a commentable_id field to store the id of the object this comment belongs to. And we add a commentable_type field to store the type of object this comment belongs to – ‘Post’, ‘Comment’, whatever. With these two pieces of info, Rails can figure out the rest and make your life easier.
Before we can add comments to our database, however, we need to make a small change the code in the “self.up” part of our migration:
def self.up create_table :comments do |t| t.string :title t.string :body t.integer :commentable_id t.string :commentable_type t.timestamps end add_index :comments, [:commentable_id, :commentable_type] endWe’ve added an single index on the combination of the commentable fields, which will speed up our app. Now we can migrate the changes again:
Now let’s setup our associations in our models, starting with posts:
class Post < ActiveRecord::Base has_many :comments, :as => :commentable endNormally if you tell Post that it has_many comments, Post would expect the comments table to have a “post_id” field. It doesn’t, because we’re using polymorphism. So we tell it the name we gave our polymorphic association: “commentable”.
Now for the slightly more complicated comment model:
class Comment < ActiveRecord::Base belongs_to :commentable, :polymorphic => true has_many :comments, :as => :commentable endFirst, we’re setting up the “belongs to” side of polymorphism. Our comment belongs to “commentable”, the name we gave our polymorphic association. We also tell it that this is polymorphic. Otherwise, Rails would look for a model called Commentable.
Finally, we’re saying that comments have many comments, the same way set did it for posts.
You might thing that after this special setup, using polymorphic associations would be more difficult as well. The good news is, the hard part is over and everything else works the same as any other “belongs_to” or “has_many” association. Let’s go into the rails console to try it out:
We’re able to add a comment to our post, and add a comment to that comment! Rails does the work of filling in the commentable_id and commentable_type fields, just as it would have filled in the post_id field if comments could only belong to posts.
Please check out the nested comments example code on github, which includes tests. Download it play around with it, and see how it works. In the next part, I’ll be looking at how to use nested comments in your controllers and views.
Tags:comments, models, nested comments, polymorphic associations, polymorphism, Ruby, ruby on rails
Posted in Database, Rails, Ruby, Testing | 11 Comments »