4

I have a many-to-many relationship between Recipes and Ingredients. I am trying to build a form that allows me to add an ingredient to a recipe.

(Variants of this question have been asked repeatedly, I have spent hours on this, but am fundamentally confused by what accepts_nested_attributes_for does.)

Before you get scared by all the code below I hope you'll see it's really a basic question. Here are the non-scary details...

Errors

When I display a form to create a recipe, I am getting the error "uninitialized constant Recipe::IngredientsRecipe", pointing to a line in my form partial

18:   <%= f.fields_for :ingredients do |i| %>

If I change this line to make "ingredients" singular

<%= f.fields_for :ingredient do |i| %>

then the form displays, but when I save I get a mass assignment error Can't mass-assign protected attributes: ingredient.

Models (in 3 files, named accordingly)

class Recipe < ActiveRecord::Base
  attr_accessible :name, :ingredient_id
  has_many :ingredients, :through => :ingredients_recipes
  has_many :ingredients_recipes

  accepts_nested_attributes_for :ingredients
  accepts_nested_attributes_for :ingredients_recipes
end

class Ingredient < ActiveRecord::Base
  attr_accessible :name, :recipe_id
  has_many :ingredients_recipes
  has_many :recipes, :through => :ingredients_recipes

  accepts_nested_attributes_for :recipes
  accepts_nested_attributes_for :ingredients_recipes
end

class IngredientsRecipes < ActiveRecord::Base
  belongs_to :ingredient
  belongs_to :recipe

  attr_accessible :ingredient_id, :recipe_id
  accepts_nested_attributes_for :recipes
  accepts_nested_attributes_for :ingredients
end

Controllers

As RESTful resources generated by rails generate scaffold

And, because the plural of "recipe" is irregular, inflections.rb

ActiveSupport::Inflector.inflections do |inflect|
    inflect.irregular 'recipe', 'recipes'
end

View (recipes/_form.html.erb)

<%= form_for(@recipe) do |f| %>
  <div class="field">
    <%= f.label :name, "Recipe" %><br />
    <%= f.text_field :name %>
  </div>
  <%= f.fields_for :ingredients do |i| %>
    <div class="field">
      <%= i.label :name, "Ingredient" %><br />
      <%= i.collection_select :ingredient_id, Ingredient.all, :id, :name %>
    </div>
  <% end %>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

Environment

  • Rails 3.2.9
  • ruby 1.9.3

Some things tried

If I change the view f.fields_for :ingredient then the form loads (it finds Recipe::IngredientRecipe correctly, but then when I save, I get a mass-assignment error as noted above. Here's the log

Started POST "/recipes" for 127.0.0.1 at 2012-11-20 16:50:37 -0500
Processing by RecipesController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"/fMS6ua0atk7qcXwGy7NHQtuOnJqDzoW5P3uN9oHWT4=", "recipe"=>{"name"=>"Stewed Tomatoes", "ingredient"=>{"ingredient_id"=>"1"}}, "commit"=>"Create Recipe"}
Completed 500 Internal Server Error in 2ms

ActiveModel::MassAssignmentSecurity::Error (Can't mass-assign protected attributes: ingredient):
  app/controllers/recipes_controller.rb:43:in `new'
  app/controllers/recipes_controller.rb:43:in `create'

and the failing lines in the controller is simply

@recipe = Recipe.new(params[:recipe])

So the params being passed, including the nested attributes, are incorrect in some way. But I have tried lots of variants that fix-one-break-another. What am I failing to understand?

Tom Harrison
  • 13,533
  • 3
  • 49
  • 77

4 Answers4

8

Thanks to clues from all, I have found what was wrong with my approach. Here's how I solved it.

I had originally tried with a simple HABTM many-to-many relationship, where the join table was named following standard Rails convention: ingredients_recipes. Then I realized that in a way, accepts_nested_attributes_for is designed for a 1-to-many relationship. So I converted to using has_many_through, creating a model IngredientsRecipes.

That name was the core problem, because Rails needs to be able to convert from plural to singular when using build to create form elements. This caused it to look for the non-existant class Recipe::IngredientsRecipe. When I changed my form so it used fields_for :ingredient the form displayed, but still failed to save with a mass assignment error. It even failed when I added :ingredients_attributes to attr_accessible. It still failed when I added @recipe.ingredients.build to RecipesController#new.

Changing the model to a singular form was the final key to resolve the problem. IngredientsRecipe would have worked, but I chose RecipeIngredients, as it makes more sense.

So to summarize:

  • can't use accepts_nested_attributes_for with has_and_belongs_to_many; need has_many with through option. (Thanks @kien_thanh)
  • adding accepts_nested_attributes_for creates a accessor that must be added to attr_accessible in the form <plural-foreign-model>_attributes, e.g. in Recipe I added attr_accessible :name, :ingredients_attributes (Thanks @beerlington)
  • before displaying the form in the new method of the controller, must call build on the foreign model after creating a new instance, as in 3.times { @recipe.ingredients.build }. This results in HTML having names like recipe[ingredients_attributes][0][name] (Thanks @bravenewweb)
  • join model must be singular, as with all models. (All me :-).
Tom Harrison
  • 13,533
  • 3
  • 49
  • 77
4

If you inspect the form that is generated, you'll notice that the nested fields have a name like "ingredients_attributes". The reason you're getting the mass-assignment error is because you need to add these fields to the attr_accessible declaration.

Something like this should fix it (you'll need to doublecheck the field names):

class Recipe < ActiveRecord::Base
  attr_accessible :name, :ingredients_attributes
  #...
end

Update: There's a similar answer here

Community
  • 1
  • 1
Peter Brown
  • 50,956
  • 18
  • 113
  • 146
  • Thanks for the clues but perhaps my brain is just addled from reading and reading and reading the docs :-P -- when I inspect the HTML of the form where I am adding ingredients to a recipe, the select list is named `recipe[ingredient][id]` which is parsed into the params as `"recipe"=>{"name"=>"Stewed Tomatoes", "ingredient"=>{"ingredient_id"=>"1"}}`. I know that's wrong, but I am not sure what is right. Is `:ingredients_attributes` kind of a collection of attributes of the Ingredients model? I just don't have a good mental model of what Rails is looking for, or even an example. Thanks. – Tom Harrison Nov 21 '12 at 02:05
  • This was a big clue, but only part of the answer, I have found answer and will add it below. Thanks!!! – Tom Harrison Nov 21 '12 at 15:30
1

Leave the call as

<%= f.fields_for :ingredients do |i| %>

But before that do

<% @recipe.ingredients.build %>

Im guessing that will allow your form to be created the right way, but there are likely other errors with your models, I can look @ it more in detail when I have more time if its still not working, but:

As far as what accepts_nested_attributes_for does, when you pass in a correctly formatted params hash to the Model.new or Model.create or Model.update, it allows those attributes on the related model to be saved if they are in the params hash. In addition though, you do need to make the attributes accessible if they are unaccessible in the parent model as stated by beerlington.

bravenewweb
  • 1,596
  • 1
  • 11
  • 6
  • Thank you! I am working now to create the simplest possible case, which may start with a one-to-many case (rather than my many-to-many case). Once I grok `accepts_nested_attributes_for` a bit more, I'll be less of an idiot than I feel like I am now ;-) – Tom Harrison Nov 21 '12 at 03:14
1

I think you just need set up a one-to-many association, one recipe has many ingredients and one ingredient belongs to one recipe, so your model look like:

class Recipe < ActiveRecord::Base
  attr_accessible :name, :ingredients_attributes
  has_many :ingredients

  accepts_nested_attributes_for :ingredients
end

class Ingredient < ActiveRecord::Base
  attr_accessible :name, :recipe_id
  belongs_to :recipe
end

You are built right form, so I don't write it again here. Now in your new and create controller will be like this:

def new
  @recipe = Recipe.new

  # This is create just one select field on form
  @recipe.ingredients.build 

  # Create two select field on form
  2.times { @recipe.ingredients.build }

  # If you keep code above for new method, now you create 3 select field
end

def create
  @recipe = Recipe.new(params[:recipe])
  if @recipe.save
    ...
  else
    ...
  end
end

How does params[:recipe] look like? If you just have one select field, maybe like this:

params = { recipe: { name: "Stewed Tomatoes", ingredients_attributes: [ { id: 1 } ] } }

If you have 2 ingredient select field:

params = { recipe: { name: "Stewed Tomatoes", ingredients_attributes: [ { id: 1 }, { id: 2 } ] } }
Thanh
  • 8,219
  • 5
  • 33
  • 56
  • Thank you! I am quite sure the one-to-many relationship will work, except that's not how my data is modelled (e.g. "tomatoes" are an ingredient in "Stewed Tomatoes" and "Marinara Sauce" recipes; "Marinara Sauce" has "tomatoes" and "garlic" ingredients). Based on the naming I am seeing in the one-to-many cases, I am beginning to think either 1) `accepts_nested_attributes_for` doesn't work for M-2-M relationships, or 2) I haven't figured out how yet :-). When I am done, I will write a blog post with a clear and simple answer, since there's *nothin'* out there on Google. – Tom Harrison Nov 21 '12 at 12:57
  • I believe that `accepts_nested_attributes_for` only work for one-one and one-to-many, as in [API](http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html) they are also not mention about many-to-many relationship. – Thanh Nov 21 '12 at 13:04
  • Lookin' that way, and now I am figuring out why, and if there's a way to make it work. Several other questions on SO suggest that people *have* gotten it to work. – Tom Harrison Nov 21 '12 at 13:18
  • I think I don't need to build nested form for many-to-many. In basic, it's still one-to-many association if you use `has_many, :through` in two model. I think that is reason why Rails did not provide `accepts_nested_attributes_for` for many-to-many relationship. – Thanh Nov 21 '12 at 14:25
  • I have found the solution to the problem, and it *does work* for many_to_many. See below. – Tom Harrison Nov 21 '12 at 15:32