Forum: Ruby on Rails has_many :though and nested attributes

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
Branko V. (Guest)
on 2009-05-15 17:40
(Received via mailing list)
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
Colin L. (Guest)
on 2009-05-15 18:03
(Received via mailing list)
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>
Branko V. (Guest)
on 2009-05-15 20:48
(Received via mailing list)
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.
Branko V. (Guest)
on 2009-05-15 21:24
(Received via mailing list)
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. :P

I'm really not sure what's going on...
Colin L. (Guest)
on 2009-05-15 21:30
(Received via mailing list)
Can anyone else help here, I haven't used nested attributes yet.

2009/5/15 Branko V. <removed_email_address@domain.invalid>
Branko V. (Guest)
on 2009-05-15 22:59
(Received via mailing list)
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?
Michael S. (Guest)
on 2009-05-15 23:20
(Received via mailing list)
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/
Branko V. (Guest)
on 2009-05-16 01:39
(Received via mailing list)
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
Michael S. (Guest)
on 2009-05-16 04:43
(Received via mailing list)
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/
Branko V. (Guest)
on 2009-05-16 12:31
(Received via mailing list)
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".
Michael S. (Guest)
on 2009-05-16 14:27
(Received via mailing list)
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/
Branko V. (Guest)
on 2009-05-16 21:56
(Received via mailing list)
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. :P

What particular section of API docs should I be reading?


Thanks for help, all.


--
Branko
Michael S. (Guest)
on 2009-05-17 00:14
(Received via mailing list)
On Saturday 16 May 2009, Branko V. wrote:
> a mixed-model form with all new objects... and many other things. :P
>
> What particular section of API docs should I be reading?

http://api.rubyonrails.org/classes/ActiveRecord/As...
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/
Branko V. (Guest)
on 2009-05-17 00:23
(Received via mailing list)
On May 16, 10:14 pm, Michael S. <removed_email_address@domain.invalid> wrote:
> http://api.rubyonrails.org/classes/ActiveRecord/As......
> 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
Branko V. (Guest)
on 2009-05-17 00:32
(Received via mailing list)
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
Branko V. (Guest)
on 2009-05-17 00:50
(Received via mailing list)
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
Branko V. (Guest)
on 2009-05-17 01:21
(Received via mailing list)
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... :(

1:
http://ryandaigle.com/articles/2009/2/1/what-s-new...
2:
https://rails.lighthouseapp.com/projects/8994-ruby...
3: http://railsforum.com/viewtopic.php?pid=96990

--
Branko
Branko V. (Guest)
on 2009-05-17 01:51
(Received via mailing list)
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 :P ).

--
Branko
Michael S. (Guest)
on 2009-05-17 02:59
(Received via mailing list)
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 :P ).

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/
Branko V. (Guest)
on 2009-05-17 19:39
(Received via mailing list)
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
This topic is locked and can not be replied to.