Almost Painless Nested Resources
Posted by Tammer Saleh
Oct 23
It’s often the case that you have a restful resource that needs to be accessible nested, and at root. For example, let’s say we have a storefront with a bunch of users and products. Each user can play with their own products, and the admin users can play with everyone’s products. You’ve got a :products resource at root, and a nested version to scope it under :users
1 2 3 4 5 6 7 |
# Admin see all products at /products map.resources :products map.resources :users do |users| # Users see theirs at /users/:id/products users.resources :products, :name_prefix => "user_" end |
That’s all fine and dandy, but your controller is gonna get really messy, real quick:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class ProductsController < ApplicationController def index if params[:user_id] @products = User.find(params[:user_id]).products else @products = Product.find(:all) end end def show if params[:user_id] @products = User.find(params[:user_id]).products.find(params[:id]) else @products = Product.find(params[:id]) end end #... end |
Now, you’ll immediately see that this can be dried up fairly nicely by taking advantage of the duck-similarities between Product and @user.products:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class ProductsController < ApplicationController before_filter :load_user def index @products = products.find(:all) end def show @products = products.find(params[:id]) end #... private def load_user @user = User.find_by_id(params[:user_id]) end def products @user ? @user.products : Product end end |
This is definitely a step in the right direction. But we get a real good kick in the face by our ActiveRecord associations. Instead of defining user.products.new(), they went with user.products.build(), completely un-drying our :new and :create actions!
Now, this will be fixed by a simple :alias_method call in Rails 2.0, but since we’re sticking with stable code for now, we’ll have to do it by hand.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
module ActiveRecord module Associations class HasManyThroughAssociation alias_method :new, :build end class HasManyAssociation alias_method :new, :build end class HasAndBelongsToManyAssociation alias_method :new, :build end end end |
Just put that in your /lib, and require it. Now you’ve got yourself a painless nested (or non) resource:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
class ProductsController < ApplicationController before_filter :load_user before_filter :authorize def index @products = products.search(params[:search]) end def show @product = products.find(params[:id]) end def new # This might use our alias... or it might not... how exciting! @product = products.new end def edit @product = products.find(params[:id]) end def create # Once again, meta programming to the rescue! @product = products.new(params[:product]) if @product.save flash[:notice] = :success redirect_to_product(@product) else render :action => "new" end end def update @product = products.find(params[:id]) if @product.update_attributes(params[:product]) flash[:notice] = :success redirect_to_product(@product) else render :action => "edit" end end def destroy @product = products.find(params[:id]) @product.destroy flash[:notice] = :success redirect_to_collection end private def authorize # blah blah deny access... end def load_user @user = User.find_by_id(params[:user_id]) end def products @user ? @user.products : Product end # These are just sugar... def redirect_to_collection redirect_to(@user ? user_products_url(@user) : products_url) end def redirect_to_product(p) redirect_to(p.user ? user_product_url(p.user, p) : product_url(p)) end end |
This should prove even easier with all of the resource and url changes coming in Rails 2.0.
Comments on this post
Oct 23
jare care said,
How come you didnt go with an ‘admin’ module and a separate controller instead of using a singular controller and conditional logic?
Oct 23
Tammer Saleh said,
The two views would be identical, only the permissions would be different. Generally, you use the admin namespace when you need an entirely different presentation for an administrative user.
That being said, I tend to feel that the whole admin namespace thing is overused. Most of the time, it’s simpler to share controllers and views, and present admins with more resources, links, etc.
Oct 23
Brian said,
This works great, the only problem I see is that often times you only want on controller to handle the crud actions. In the other views you often want to only view or show. I’ve been using make_resourceful which is great, factors out all recurring bits of code. In it you can use a conditional statement in the current_model to accomplish the same thing.
Oct 23
Bobby Cocknocker said,
+1 vote for make_resourceful.
It can do everything you posted in just a few painless lines of code.
Oct 23
Tammer Saleh said,
Well, that does it. I’ll break out make_resourceful for the next project I work on… Put it through it’s paces.
Oct 24
Michael said,
In turn I’d recommend resources controller (http://agilewebdevelopment.com/plugins/resources_controller) over make_resourcesful, through that one is good, too.
Oct 24
James Golick said,
There’s also resource_controller (I didn’t know about resources_controller when I named it). It has support for nested and polymorphic resources, and it generates polymorphic urls for free too! It’s also tested with shoulda :)
AND, coming this week, a scaffold generator for resource_controller controllers, and shoulda tests!
Oct 24
Tammer Saleh said,
James – I really liked your smart_url plugin. I’ll have to do a roundup of all of the new rest systems. It’d make a good post.
Oct 31
EmmanuelOga said,
Yet another solution for the same issue:
http://code.google.com/p/autorest/
My approach is much simpler, i don’t use a DSL for the specification of the controller, but simple configuration plus include plus some generic behavior very related to the thing you describe—see:
http://autorest.googlecode.com/svn/trunk/lib/components/instance_methods/for_model_manipulation.rb
Also see my blog post, if you want:
http://emmanueloga.wordpress.com/2007/10/31/restfull-development-simplified/
Greetings!
Sorry, comments are closed for this article.
© 2000 - 2009 by thoughtbot, inc.
written by a bushel of tiny robots
Come “ride the toad” on Hoptoad, the app error app.
Thunder Thimble: Brand monitoring for social media.
Widgetfinger: Simple content management for simple websites.
Tee-Bot, funny shirts your friends won't understand!
Umbrella Today: “It’s like totally the simplest weather report ever, Julie.”
Thoughtbot
thoughtbot is a technology consulting firm that provides web application development and design services. We focus on building modern systems, embracing good ideas and delivering elegant solutions.
Interested in learning Rails?
Sign up for our beginning or advanced training.
Archives