Almost Painless Nested Resources

Tammer Saleh

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

# 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:

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:

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.

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:

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.