has_many :though and nested attributes

Hi all,

I have a rather weird problem. Three models in has_many :through relationship:

Nutrient <-- IngredientsNutrient --> Ingredient

    Nutrient has_many :ingredient_nutrients     Nutrient has_many Ingredients, :through => :ingredient_nutrients     Ingredient has_many :ingredient_nutrients     Ingredient has_many :nutrients, :through => :ingredient_nutrients     IngredientsNutrient belongs_to :nutrient     IngredientsNutrient belongs_to :ingredient

IngredientsNutrient is a model, because I need to specify the quantity of nutrients per ingredient.

With this, I wanted to create a form for Ingredient model which includes IngredientsNutrient fields for each Nutrient.all.

I've added to Ingredient:

    accepts_nested_attributes_for :ingredients_nutrient

and went on to add in the view:

    <% f.fields_for :ingredients_nutrient do |in_f| %>        ....     <% end %>

In the controller, I have the following code:

    for nutrient in Nutrient.all        @ingredient.ingredients_nutrients.build({ :nutrient_id => nutrient.id })     end

When I submit the form filling out all the fields, it fails validation saying that ingredient_id cannot be blank (exact message is: "Ingredient nutrients ingredient can't be blank").

My initial assumption was that the ingredient_id needs not be specified, but either Rails doesn't think so, or I have made an error in coding. I've double checked Ryan's examples of nested attributes, but I can't see anything. The only thing I've noticed that he didn't deal with has_many-through type of relationship.

Can anyone please point me in the right direction?

Thanks,

I don’t know whether this is part of the problem or just that your question has several typos.

I think the class should be IngredientNutrient not IngredientsNutrient and the controller should be ingredient_nutrients. You have them in various combinations throughout the post.

Colin

Thanks, Colin. I'll try IngredientNutrient. Could be that *that's* the reason it's not working.

It makes no difference, but then again, it's logical. No matter how incredibly stupid a model's name is, if you use it consistently (which I did in live code) it works the same way as a smart name. :stuck_out_tongue:

I'm really not sure what's going on...

Can anyone else help here, I haven’t used nested attributes yet.

Can't find a single example using has_many-through. Is there any other way to get multi-model forms work for my setup?

I haven't looked into your problem in depth, but

class Ingredient < ...   accepts_nested_attributes_for :ingredient_nutrient end

doesn't help, because you're not *creating* ingredients with associated ingredient_nutrients. Rather, you're associating existing ingredients and nutrients with the help of ingredient_nutrients.

I think in order to get better help you ought to post more of your code and the exact exception messages you get.

Michael

I'm starting to think this might be a design problem...

Back to square 1, what I'm trying to do is the following:

I have a bunch of _nutrients_ which are defined by their `name` and the `units` used to measure them. I have _ingredients_ that are defined by their `name`, and some meta data irrelevant for this setup. Finally I wish to be able to enter the ingredients and associated nutritional information (i.e., the nutrients and their quantities) in one go.

The initial plan was to have all nutrients as columns in the ingredients table, but then we realized that the number of nutrients may change from time to time, and that's why I opted for the above setup.

Here's the model code that I'm using right now (renamed the awkward "IngredientNutrient" to "Nutrition"):

    class Ingredient < ActiveRecord::Base       has_many :nutritions       has_many :nutrients, :through => :nutritions

      accepts_nested_attributes_for :nutritions

      validates_presence_of :name, :kind, :density       validates_uniqueness_of :name       validates_length_of :name, :maximum => 40       validates_inclusion_of :kind, :in => [0, 1, 2]     end

    class Nutrient < ActiveRecord::Base       has_many :nutritions       has_many :ingredients, :through => :nutritions

      validates_presence_of :name, :unit       validates_uniqueness_of :name       validates_length_of :name, :maximum => 40       validates_inclusion_of :unit, :in => %w( g mg mcg IU ml )     end

    class Nutrition < ActiveRecord::Base       belongs_to :nutrient       belongs_to :ingredient

      validates_presence_of :nutrient_id, :ingredient_id, :quantity     end

The snippet from the Ingredients controller:

    def new       @ingredient = Ingredient.new       for nutrient in Nutrient.all         @ingredient.nutritions.build :nutrient_id => nutrient.id # <-- this is probably bad?       end     ....     end

And finally the _form partial:

    <p>       <%= f.label :name %><br />       <%= f.text_field :name %>     </p>     <p>       <%= f.label :kind %><br />       <%= f.text_field :kind %>     </p>     <p>       <%= f.label :density %><br />       <%= f.text_field :density %>     </p>     <p>       <%= f.label :comments %><br />       <%= f.text_area :comments %>     </p>

    <% f.fields_for :nutritions do |n_f| %>       <p>       <%= n_f.hidden_field :nutrient_id %>       <%= n_f.text_field :quantity %>       </p>     <% end %>

> doesn't help, because you're not *creating* ingredients with > associated ingredient_nutrients. Rather, you're associating > existing ingredients and nutrients with the help of > ingredient_nutrients.

I'm starting to think this might be a design problem...

Back to square 1, what I'm trying to do is the following:

I have a bunch of _nutrients_ which are defined by their `name` and the `units` used to measure them. I have _ingredients_ that are defined by their `name`, and some meta data irrelevant for this setup. Finally I wish to be able to enter the ingredients and associated nutritional information (i.e., the nutrients and their quantities) in one go.

You're not saying what doesn't work with your code as it is.

The snippet from the Ingredients controller:

    def new       @ingredient = Ingredient.new       for nutrient in Nutrient.all         @ingredient.nutritions.build :nutrient_id => nutrient.id # <-- this is probably bad?

You could just write

  @ingredient.nutritions.build(:nutrient => nutrient)

Michael

Got it. But that doesn't solve the problem, does it?

Btw, the error message displayed by the form is "Nutrients ingredient can't be blank".

You know, this is a bit like pulling teeth. When are you getting this message? Presumably you're trying to save something. What objects are there? Which of them are new and unsaved (new_record? == true), which of them already exist in the database (new_record? == false), but need saving, which are unchanged. Finally, how are you saving them.

I can't point to any specific problem, but I suspect you're running into non-intuitive issues related to when objects and their associated objects are saved, in particular, if there are new objects in the mix. The API docs as well as Agile Web Dev with Rails have sections on this topic, it might be a good idea to review them. This is just a hunch, the cause of your problem may be somewhere else entirely.

Michael

> Btw, the error message displayed by the form is "Nutrients ingredient > can't be blank".

You know, this is a bit like pulling teeth. When are you getting this

Ouch. Sorry, I'm sort of new to Rails, so I don't know what messages I can acquire, and where to look for them.

message? Presumably you're trying to save something. What objects are there? Which of them are new and unsaved (new_record? == true), which of them already exist in the database (new_record? == false), but need saving, which are unchanged. Finally, how are you saving them.

I have a blank database. Then I create 2 Nutrient objects so the forms for Nutritions (the join model) appear as expected. Two text boxes accompanied by two hidden fields. All other objects I'm trying to create (1 x Ingredient + 2 x Nutritions) are new.

I can't point to any specific problem, but I suspect you're running into non-intuitive issues related to when objects and their associated objects are saved, in particular, if there are new objects in the mix. The API docs as well as Agile Web Dev with Rails have sections on this topic, it might be a good idea to review them. This is just a hunch, the cause of your problem may be somewhere else entirely.

Well, I'm definitely not familiar with how Rails internally handles the order of creation of new objects when using has_many-through and a mixed-model form with all new objects... and many other things. :stuck_out_tongue:

What particular section of API docs should I be reading?

Thanks for help, all.

Look for "Unsaved objects and associations"

If you don't know this stuff yet (or, like me, keep forgetting the details), it is a very good idea to read up on it every now and then, even if it isn't necessarily responsible for your current problem.

Michael

Thanks for the link.

Meanwhile, I suspect the problem is not (just) the unsaved objects. Judging from the error message, I've a feeling that with my setup Rails is having problems realizing that the nested attributes are for objects related to the object being created by the rest of the form.

Here's the request that triggers the error:

Parameters: {   "commit"=>"Create",   "authenticity_token"=>"4UoqTR94w4Nf8LcNwgeqFfUDm7fZ +UvYeQDrfrflolw=",   "ingredient"=>{     "kind"=>"1",     "name"=>"test",     "comments"=>"",     "nutritions_attributes"=>{       "0"=>{         "quantity"=>"123",         "nutrient_id"=>"1"         }       },     "density"=>"123"}   }

So the accepts_nested_attributes_for is obviously not doing what I thought it does (which is, create associated objects from `objectname_attributes` params)...

Ok, it's becoming a bit clearer to me now.

a = Ingredient.new

=> #<Ingredient id: nil, name: nil, kind: nil, density: nil, comments: nil, weight_per_piece: nil>

a.nutritions_attributes = [{ :nutrient_id => 1, :quantity => 123 }]

=> [{:nutrient_id=>1, :quantity=>123}]

a.name = 'test'

=> "test"

a.kind = 1

=> 1

a.density = 2

=> 2

a.valid?

=> false

a.errors

=> #<ActiveRecord::Errors:0xb72b5e78 @errors= {"nutritions_ingredient_id"=>["can't be blank"]}, @base=#<Ingredient id: nil, name: "test", kind: 1, density: #<BigDecimal:b72a56cc,'0.2E1', 4(8)>, comments: nil, weight_per_piece: nil>>

I can save the Ingredient instance while no Nutrition instances are attached to it (which makes heck of a lot of sense)...

I carefully read all the comments on Ryan's Scraps.[1] A similar issue filed as a ticked on lighthouse[2] is marked as 'wontfix', so I assume it has nothing to do with nested_attributes, but with how Rails work in general. I'm still having trouble figuring out why and how, but it seems I'm not alone[3]... which kinda sucks... :frowning:

1: http://ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes 2: #1943 Nested attributes validations circular dependency - Ruby on Rails - rails 3: http://railsforum.com/viewtopic.php?pid=96990

I decided to remove the

    validates_presence_of :nutrient_id, :ingredient_id

from the Nutrition's validation. And this works.To make sure I can't save a blank _id field, I'll add :allow_null => false to references columns. (I didn't do it before because I didn't know it was possible :stuck_out_tongue: ).

Yes, probably your best bet in cases like this is to ensure consistency at the database level (you should do this anyway) and wrap a transaction block around the database manipulations. It may help to create/update objects piecemeal instead of trying to build a graph of objects and save them with a single object.save.

Michael

I wanted to keep the controllers as clean as possible. In this case, it makes a lot of sense to have the nutrients entered from the ingredients page. So, the only options I had left were to add a new method to the Ingredient model (possibly after_save callback?), or do it the way I did... The other option seemed like a more elegant solution probably because I'm coming from Django with those inline admin forms goodness. :)))