Rails: Nested resource scaffold
23 Jan, 2007

In my previous post I told you about the resource scaffold. What you’ll be doing a lot is nesting these resources. Ingredients in recipes, comments on posts, options for products. You name it, you nest it!

Since Rails does not automatically nest resources for you, you should do this yourself. This is, with some minor tweaks, really easy to accomplish. In this example I’ll create recipes that have multiple ingredients.

I assume you have Rails 1.2.1 installed for this tutorial to work properly.

First, I create an new rails project named ‘cookbook’. I use an SQLite3 database because it’s easy to do so. You may use any Rails compatible database for this example.

$mkdir cookbook rails --database sqlite3 cookbook cd cookbook First I create resource scaffolds for both the Recipe and Ingredient models:$ ./script/generate scaffold_resource Recipe title:string instructions:text
./script/generate scaffold_resource Ingredient name:string quantity:string

As you can see I did not add a recipe_id to the ingredient model because of the has_many relationship. Add this column to the migration file. You should now be able to migrate your database:

\$ rake db:migrate

If you add the recipe_id to the generate script the view for your ingredients will include a field for the recipe_id and that’s not what you want.

Next, make the has_many relationship in your models.

app/models/recipe.rb:

class Recipe < ActiveRecord::Base
has_many :ingredients
end

app/models/ingredient.rb

class Ingredient < ActiveRecord::Base
belongs_to :recipe
end

So far, nothing new. Next we check out config/routes.rb:

map.resources :ingredients
map.resources :recipes

What we want is to map ingredients as a resource to recipes. Replace these two lines with:

map.resources :recipes do |recipes|
recipes.resources :ingredients
end

This will give you urls like /recipes/123/ingredients/321

Now we need to make some changes to the ingredients controller. Every ingredient belongs to a recipe. First add the filter:

before_filter(:get_recipe)

private
def get_recipe
@recipe = Recipe.find(params[:recipe_id])
end

This will make sure that every ingredient knows what recipe it belongs to.

In the index method of the ingredient controller, make sure you have this:

@ingredients = @recipe.ingredients.find(:all)

This makes sure you only show ingredients for this recipe, and not all ingredients in the database.

Because we changed the route for ingredients, we need to update all ingredient_url() and ingredient_path() calls in our controller and views. Change all occurrences of

ingredient_url(@ingredient)
and
ingredient_path(@ingredient)

to

ingredient_url(@recipe, @ingredient)
and
ingredient_path(@recipe, @ingredient)

Note: Make sure that you don’t replace ‘ingredient’ with ‘@ingredient’ in your views!