Rails: Nested resource scaffold
23 January 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.
class Recipe < ActiveRecord::Base has_many :ingredients end
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
Note: Make sure that you don’t replace ‘ingredient’ with ‘@ingredient’ in your views!
Add a link to the ingredients to your recipe’s index.rhtml view.
link_to 'Ingredients', ingredients_path(recipe)
And, at last, make sure the create method of your ingredients controller attaches the ingredient to the right recipe. Make sure the first two lines look like this:
def create @ingredient = @recipe.ingredients.new(params[:ingredient])
You may now start your webserver and check out http://localhost:8000/recipes . Create some recipes and click ‘ingredients’. You can now add ingredients for this recipe!
The next step will be customizing each method to suit your own needs.