Welcome to Giant Robots Smashing Into Other Giant Robots — a weblog about development, business, design and technology — written by thoughtbot.

Almost Painless Nested Resources

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.


About this entry

 

thoughtbot is hiring

We are hiring web developers and web designers in both Boston and New York, NY.

What are we up to?

We built Shoulda, an eclectic set of additions to Test::Unit; Paperclip to manage uploaded files without hassle; Jester, a REST/ActiveResource client library written in Javascript, and Squirrel, an enhancement for ActiveRecord's find syntax; — amongst some other projects.


Chad (President) and Jon (CTO) co-authored a technical book titled Pro Active Record: Databases with Ruby and Rails, which explores the ins and outs of the ActiveRecord ruby library. You can buy it today at Amazon.com.

About thoughtbot, inc.

We are a small web application development consulting business, with offices in Boston, MA and New York, NY. If you're looking to find a team for your next web development project or your new web application — get in touch.