Subscribe to my Feed, follow me on Twitter, recommend me on Working With Rails or see my code on GitHub
Cleaning up your nest: How to nest resources with multiple access points
With Rails’ new routes nesting resources is easy. Now, whether you should nest or not depends on a lot of things, and others have written smart things about that. Sometimes though, nesting makes sense, and sometimes resources can have multiple access points. In REST terminology, they’re actually different resources, even though in Rails they use the same model. A classic example is this:
class User < ActiveRecord::Base
has_many :posts
end
class Category < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
belongs_to :category
end
Here, it makes sense to list posts in a certain category and posts that are written by a certain user in addition to just listing all posts. But how do you represent this in a RESTful manner in Rails? The basic assumption seems to be that a controller maps to a model, but I don’t think that’s the best way to do it. If you have multiple resources that use the same model, how would you know in which context the controller is used? For
PostsController in this example, how would you limit the posts listed based on a user or a category? Sure, it’s doable, but it can get pretty messy. Instead, I suggest thinking of a controller as a resource access point. That is, controllers map to resources. So, in this example, we would have a separate controller for each of these resources:
#/posts
class PostsController < ApplicationController
def index
@posts = Post.find(:all)
end
end
#/category/:category_id/posts
class CategoryPostsController < ApplicationController
before_filter :find_category
def index
@posts = @category.posts
end
private
def find_category
@category = Category.find(params[:category_id])
end
end
#/users/:user_id/posts
class UserPostsController < ApplicationController
before_filter :find_user
def index
@posts = @user.posts
end
private
def find_user
@user = User.find(params[:user_id])
end
end
Then we map all the resources in routes.rb, specifying which controller to use for the nested resources. We also add a
:name_prefix to the nested resources so the names don’t clash with the other resources with the same name.
ActionController::Routing::Routes.draw do |map|
map.resources :posts
map.resources :users do |user|
user.resources :posts, :controller => 'user_posts', :name_prefix => 'user_'
end
map.resources :categories do |category|
category.resources :posts, :controller => 'category_posts', :name_prefix => 'category_'
end
end
But that’s not DRY
There’s a lot of repetition here, but who cares? The controllers have clearly separated concerns because they represent different resources. For
UserPostsController you may not want to show a single post for example, but instead, in /user/:user_id/posts, point to /posts/:id. Repeating yourself is much better than getting tangled up in increasingly complex abstractions.
