Has_many :though and nested attributes


#1

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,


Branko


#2

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

2009/5/15 Branko V. removed_email_address@domain.invalid


#3

On May 15, 4:02 pm, Colin L. removed_email_address@domain.invalid wrote:

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.

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


#4

On May 15, 6:47 pm, Branko V. removed_email_address@domain.invalid wrote:

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…


#5

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

2009/5/15 Branko V. removed_email_address@domain.invalid


#6

On May 15, 7:29 pm, Colin L. removed_email_address@domain.invalid wrote:

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?


#7

On Friday 15 May 2009, Branko V. wrote:

On May 15, 7:29 pm, Colin L. removed_email_address@domain.invalid wrote:

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


Michael S.
mailto:removed_email_address@domain.invalid
http://www.schuerig.de/michael/


#8

On May 15, 9:20 pm, Michael S. removed_email_address@domain.invalid wrote:

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.

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 %>


Branko


#9

On Friday 15 May 2009, Branko V. wrote:

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


Michael S.
mailto:removed_email_address@domain.invalid
http://www.schuerig.de/michael/


#10

On May 16, 2:42 am, Michael S. removed_email_address@domain.invalid wrote:

@ingredient.nutritions.build(:nutrient => nutrient)
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”.


#11

On Saturday 16 May 2009, Branko V. wrote:

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


Michael S.
mailto:removed_email_address@domain.invalid
http://www.schuerig.de/michael/


#12

On Saturday 16 May 2009, Branko V. wrote:

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?


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


Michael S.
mailto:removed_email_address@domain.invalid
http://www.schuerig.de/michael/


#13

On May 16, 12:26 pm, Michael S. removed_email_address@domain.invalid wrote:

On Saturday 16 May 2009, Branko V. wrote:

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.


Branko


#14

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)…


Branko


#15

On May 16, 10:14 pm, Michael S. removed_email_address@domain.invalid wrote:

http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMet
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.

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.


Branko


#16

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:
https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/1943
3: http://railsforum.com/viewtopic.php?pid=96990


Branko


#17

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: ).


Branko


#18

On Saturday 16 May 2009, Branko V. wrote:

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


Michael S.
mailto:removed_email_address@domain.invalid
http://www.schuerig.de/michael/


#19

On May 17, 12:59 am, Michael S. removed_email_address@domain.invalid wrote:

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.

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. :)))


Branko


#20

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)…


Branko