Home > Features, General > Rails: Nested resource scaffold

Rails: Nested resource scaffold

January 23rd, 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!

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.

Please share the love of this post by bookmarking it, and sharing it with others. Thanks!

  • Digg
  • del.icio.us
  • description
  • Reddit
  • Facebook
  • E-mail this story to a friend!
  • TwitThis

  1. Ibrahim Ahmed
    January 24th, 2007 at 10:17 | #1

    Good article, we need more information about resources as the api docs are not very informative yet.

    I have a question, what the difference between _url and _path ?

    And I got a habit to create new child records as scoped from their parents. so the create method could be:

    def create
    @ingredient = @recipe.ingredients.create(params[:ingredient])

  2. January 24th, 2007 at 10:53 | #2

    @Ibrahim: the different between _url and _path is that the first constructs a full URL, including you host or domainname. The second merely returns a path like /recipes/123 without domain information.

    You second point is a good one. I’ll have to update the article later today. Other methods like edit or destroy should also use a format like this:

    @ingredient = @recipe.ingredients.find(:params[:id] to make sure you only access the actual ingredients for a certain recipe.

  3. Brandon
    January 24th, 2007 at 17:30 | #3

    Good writeup. Having a slight issue with the _path that is being created. I have a Client that has_many Matters. On my show page for my client I built in a form for adding new matters. The form has: matters_path(@client, @matter)) do |f| %> which builds the url /matters/ rather than /clients/1/matters/ which is what I need for the proper save. Any advice?

  4. January 24th, 2007 at 21:59 | #4

    @Brandon: I would first make sure the regular ‘new’ form for matters works. Next, render the form as a component on the show-view of your client:

    < %= render_component :controller => ‘matters’, :action => ‘new’ %>

    I thinks that should do the trick. If not, make sure you have made the proper change to config/route.rb

  5. February 7th, 2007 at 08:07 | #5

    Thanks, very happy to see RESTfull stuff popping up. I’ve been tinkering with this code a bit, and i’ve place the new ingredient form right into the show.rhtml of the recipe… now i’m stuck at trying to display error messages. I tried in the show.rhtml of the recipes views (where I put my form for the ingredient) and in my ingredient controller in the create method I put redirect_to recipe_url(@recipe) but cant seem to pass the errors… any ideas?

  6. AndyV
    February 7th, 2007 at 16:53 | #6

    You only need the ‘parent’ resource (ie., recipe for ingredient) when you’re doing a list or create. Otherwise, you should be able to access the ‘child’ resource directly through it’s id.

    If you need the ‘parent’ elsewhere, such as for generating the _path or _url, you can use the corresponding method on the child. Per your examples, you could have…

    ingredient_path(@ingredient.recipe, @ingredient)

    Note that this will work only for has_many (not habtm).

  7. SDG
    February 12th, 2007 at 20:52 | #7

    You have a typo in this article:

    In this method

    def get_recipe

  8. February 14th, 2007 at 11:04 | #8

    @SDG: You’re right about that typo. I’ve also made some other changes that I found made more sense, especially the part where ingredients are found for a give recipe. This seems to be the part where you had trouble.

    def index
    @ingredients = @recipe.ingredients.find(:all)
    end

    The find(:all) part makes sure all ingredients are loaded. Let me know if that fixes your problem.

  9. Dom
    February 14th, 2007 at 16:28 | #9

    Thank you so much for sharing this information - I was wondering how this was achieved. I’m eternally grateful.

  10. Jim Benton
    March 8th, 2007 at 21:05 | #10

    I had to use @ingredient = @recipe.ingredients.build(params[:ingredient]) to get this to work- calling new didn’t add the correct recipe_id to the ingredient.

  11. March 19th, 2007 at 14:11 | #11

    I agree that nested routes are a thing of beauty, but once nested it seems impossible to ‘un-nest’ a resource.

    What I am getting at is that I would like to

    a) show the ingredients for one recipe. this works fine.

    b) show a list of ingredients covering all recipies.

    This latter doesn’t seem possible. Logically, the url would become /ingredients but these can only exist inside recipes (giving /recipes/2/ingredients).

    Do you know a way to have both? List all ingredients for one recipe AND list them all for all recipes?

  12. March 20th, 2007 at 00:17 | #12

    @joost: Just add map.resoureces :ingredients to your config/routes.rb

    You’ll have ingredients twice there, but that’s no problem at all. I was amazed by this too when I found out, but it works perfectly.

  13. May 9th, 2007 at 21:04 | #13

    Just wanted to mention that in the latest Rails trunk, you have to prefix a nested resource with its parent. So ingredient_path(@recipe, @ingredient) will become recipe_ingredient_path(@recipe, @ingredient).

  14. May 22nd, 2007 at 20:47 | #14

    I guess the new stuff in trunk is to address problems like joost was having. that way it separates the calls to all recipes ingredients_path & recipe_ingredients_path(@recipe).

  15. August 30th, 2007 at 18:22 | #15

    brandon wrote:

    … Having a slight issue with the _path that is being created. I have a Client that has_many Matters. On my show page for my client I built in a form for adding new matters. The form has: matters_path(@client, @matter)) do |f| %> which builds the url /matters/ rather than /clients/1/matters/ which is what I need for the proper save. Any advice?

    I have been working on this same issue today the solution I found was

    url_for([@cient.matter, @matter]) do |f| -%>

    This generates the expected post URL /client/:client_id/matters so the controller can user the params[client_id] to scope the new matter to the correct client!

    hope this helps

  16. August 31st, 2007 at 11:24 | #16

    oops the code in my comment should be:

    url_for([@matter.client, @matter]) do |f|

  17. Brian
    December 11th, 2007 at 23:13 | #17

    What if recipes where in books. so you wanted book/:id/recipe/:id/ingredients
    How would the route.rb look then - can you double nest or should you just not do this? We have a lot of these situations.

  18. December 12th, 2007 at 07:45 | #18

    @Brian: Just like you nested ingredients into recipes, you can nest recipes in books. Note that this means that you’ll have to use books_recipe_ingredients_url(@book, @recipe, @ingredient))

    You can do this. But if you stuff the same recipies in different books, I wouldn’t nest ‘em.

    Good luck with your app.

  19. Santiago GL
    December 23rd, 2007 at 18:32 | #19

    I am using Rails 1.2.4.
    When recipes_controller.rb does NOT exist, the mapping to /recipes/1/ingredients does show the ingredients that belong to recipe 1.
    That’s ok!
    But when recipes_controller.rb DOES exist, the nested resource (/recipes/1/ingredients) outputs

    Unknown action \ No action responded to 1

    If I call /recipes/1.xml/ingredients it outputs a routing error, that is, there is no route to that resource.
    What am I doing wrong?
    Thanks

  1. January 30th, 2007 at 18:32 | #1
  2. April 21st, 2007 at 13:40 | #2