Rails 3, many-to-many, using accepts_nested_attributes_for, how do I configure correctly?

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

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

Before you get scared of the whole code below, I hope you see that this is really a basic question. These are not scary details ...

Mistakes

When I show the form for creating a recipe, I get the error message "uninitialized constant Recipe :: IngredientsRecipe", pointing to a line in my form

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

If I change this line to make the "ingredients" the only ones

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

then the form will be displayed, 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, since the plural of the β€œ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

Tried some things

If I change the view of f.fields_for :ingredient , then the form loads (it correctly finds Recipe::IngredientRecipe , but then when I save, I get a mass assignment error as above). Here is the magazine

 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 failed lines in the controller are just

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

Therefore, the passed parameters, including nested attributes, are incorrect. But I tried many options that fix the one-time-another. What? I do not understand?

+4
source share
4 answers

Thanks to the prompts of everyone, I discovered that this was not the case with my approach. This is how I solved it.

I initially tried with the simple HABTM many-to-many relationship, where the join table was called the following standard Rails convention: ingredients_recipes . Then I realized that in a sense, accepts_nested_attributes_for is for a one-to-many relationship. So I switched to using has_many_through by creating the IngredientsRecipes model.

This name was the main problem, because Rails should be able to convert from plural to unit when using build to create form elements. This made him search for the nonexistent Recipe::IngredientsRecipe class. When I changed the form, it used fields_for :ingredient form displayed, but still could not be saved 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 single form was the final key to solving the problem. IngredientsRecipe would work, but I chose RecipeIngredients , as that makes sense.

So, we summarize:

  • cannot use accepts_nested_attributes_for with has_and_belongs_to_many ; must has_many with through option. (Thanks @kien_thanh)
  • adding accepts_nested_attributes_for creates an accessor that should be added to attr_accessible in the form <plural-foreign-model>_attributes , for example. in Recipe I added attr_accessible :name, :ingredients_attributes (Thanks @beerlington)
  • before displaying the form in the new method of the controller, it is necessary to call build in the foreign model after creating a new instance, as in 3.times { @recipe.ingredients.build } . This causes HTML to have names like recipe[ingredients_attributes][0][name] (Thanks @bravenewweb)
  • The join model must be singular, as with all models. (All of me :-).
+8
source

If you check the generated form, you will notice that the nested fields have a name of the type "components_attributes". The reason you get the mass assignment error is because you need to add these fields to the attr_accessible .

Something like this should fix it (you will need to double the field names):

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

Update : here is a similar answer

+4
source

Leave the call as

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

But before that do

 <% @recipe.ingredients.build %> 

I suppose that this will allow you to form your form correctly, but other errors with your models are possible, I can take a closer look at @ when I have more time if it still does not work, but:

As for accepts_nested_attributes_for, when passing the correct formatted params hash to Model.new or Model.create or Model.update, it allows you to save these attributes in the corresponding model, if they are in the hash parameters. In addition, however, you need to make attributes available if they are not available in the parent model, as indicated in the peelington.

+1
source

I think you just need to create a one-to-many association, one recipe contains many ingredients, and one ingredient belongs to one recipe, so your model looks like this:

 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 have built the correct form, so I am not writing it here. Now in your new create controller there will be the following:

 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 

What does params[:recipe] look like? If you have only one field for selection, perhaps like this:

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

If you have an ingredient selection box:

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

Source: https://habr.com/ru/post/1447352/


All Articles