[Rails 2.1] association.build has to be "committed" by calling association.length or association.each

Hi,

I very careful about calling something a bug, so there's of course the possibility that I'm simply doing something wrong, but I've encountered some strange behavior:

I have a model with a has_many association, let's call them customer and orders. When I call customer.orders.build on an existing customer that doesn't have any orders yet, and then want to let a partial iterate over the orders to render each one, nothing happens. When, however, I call customer.orders.length or customer.orders.each {} right after build, it works.

So if I have a customer that doesn't have any orders yet and I do this:

controller: @customer.orders.build

view: <%= render(:partial => 'order', :collection => @customer.orders) %>

... no order will be rendered.

When I do this, the order will be rendered:

controller: @customer.orders.build @customer.orders.length

view: <%= render(:partial => 'order', :collection => @customer.orders) %>

I am using Rails 2.1. Is this a known issue? Am I doing something wrong/misunderstanding something?

Could it possibly be related to this ticket in your old trac: http://dev.rubyonrails.org/ticket/10203 ?

- Johannes

It would be loading the associated objects without checking to see if they are a #new_record?... seems like a bug to me.

Should I open a ticket for this?

I am precisely documenting associations these days.

I think the problem is not related to #build itself, that method adds the model to the target of the proxy just fine:

   fxn@feynman:~/tmp/test_associations$ cat test_build.rb    post = Post.create    post.comments.build    puts post.comments.target.empty?    fxn@feynman:~/tmp/test_associations$ script/runner test_build.rb    false

Looks like the issue has to do with rendering collections somehow, if you pass the target to the helper instead it works:

   <%= render :partial => "comment", :collection => @post.comments.target %>

Note that target is a getter for @target, load_target is not involved.

Looking further down, render_partial_collection has this line

   return " " if collection.empty?

empty? is equivalent to size.zero? for association collections and #size does some stuff, so I guess we are getting close. The branch we enter does take into account new records and performs a count_records in case there was something in the database, the result is correct. In fact the flow execution goes on, but *after* that line collection.empty? is true (so the #map call afterwards does nothing and no template gets rendered). My conjecture is that the call to #size resets the proxy somehow.

I may complete the walkthrough later but that's what I've got by now. Next target is count_records.

Bingo, I could continue a few minutes:

        def count_records           # ...           @target = and loaded if count == 0           # ...         end

I think the fix is just to delete that line, looks like premature optimization, though I've not tested whether deleting it breaks something else.

Correct, a simple way to depict the underlying problem is:

   fxn@feynman:~/tmp/test_associations$ cat test_build.rb    post = Post.create    post.comments.build    puts post.comments.size    puts post.comments.size    fxn@feynman:~/tmp/test_associations$ script/runner test_build.rb    1    0

I'll write a patch tonight if nobody did.

I am precisely documenting associations these days.

I think the problem is not related to #build itself, that method adds the model to the target of the proxy just fine:

  fxn@feynman:~/tmp/test_associations$ cat test_build.rb   post = Post.create   post.comments.build   puts post.comments.target.empty?   fxn@feynman:~/tmp/test_associations$ script/runner test_build.rb   false

Looks like the issue has to do with rendering collections somehow, if you pass the target to the helper instead it works:

  <%= render :partial => "comment", :collection =>
@post.comments.target %>

Note that target is a getter for @target, load_target is not involved.

Looking further down, render_partial_collection has this line

  return " " if collection.empty?

empty? is equivalent to size.zero? for association collections and #size does some stuff, so I guess we are getting close. The branch we enter does take into account new records and performs a count_records in case there was something in the database, the result is correct. In fact the flow execution goes on, but *after* that line collection.empty? is true (so the #map call afterwards does nothing and no template gets rendered). My conjecture is that the call to #size resets the proxy somehow.

Size looks like this:

  def size     if @owner.new_record? || (loaded? && !@reflection.options[:uniq])       @target.size     elsif !loaded? && !@reflection.options[:uniq] && @target.is_a? (Array)       unsaved_records = Array(@target.detect { |r| r.new_record? })       unsaved_records.size + count_records     else       count_records     end   end

Unrelated to this and maybe I'm hallucinating, but isn't that second
branch wrong ? should it not be unsaved_records = @target.select { |r| r.new_record? }) (since detect just returns the first element for which the block
returns true, or nil)

If we look inside count_records we can see that your conjecture is
correct. count_records gets the count (reads the counter cache or
executes the relevant sql query) and then it does

@target = and loaded if count == 0

My reasoning is that this is an optimisatiohn: if you do foos.count
and you get back 0 then you can assume that foos is indeed an empty
array - there is no need to actually load the foos array. Of course in
this case this optimisation is screwing things up.

@target ||= and loaded if count == 0

might be more correct.

Fred

Can you fix the other problem while you're in there? currently if you do

post = Post.create post.comments.build puts post.comments.size

the output is 1

Fred

Here we go!

http://rails.lighthouseapp.com/projects/8994/tickets/865-fix-count_records#ticket-865-1

Sure!

I couldn't reproduce this one on edge. It turns out it was fixed in this patch:

   #305 Size reports incorrectly for collections when more than one item is added with build - Ruby on Rails - rails

The culprit was precisely the detect/select stuff you pointed out (well, I guess your example was indeed hand made to show a consequence of the detect call.)

Can you fix the other problem while you're in there? currently if you do

post = Post.create post.comments.build post.comments.build puts post.comments.size

the output is 1

Sure!

I couldn't reproduce this one on edge. It turns out it was fixed in
this patch:

  #305 Size reports incorrectly for collections when more than one item is added with build - Ruby on Rails - rails

Ah, awesome. Sorry for the false alarm!

Fred