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

jare care

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?

Tammer Saleh

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.

Brian

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.

Bobby Cocknocker

Oct 23

Bobby Cocknocker said,

+1 vote for make_resourceful.

It can do everything you posted in just a few painless lines of code.

Tammer Saleh

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.

Michael

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.

James Golick

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!

Tammer Saleh

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.

EmmanuelOga

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