[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:

   http://rails.lighthouseapp.com/projects/8994/tickets/305-size-reports-incorrectly-for-collections-when-more-than-one-item-is-added-with-build

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:

  http://rails.lighthouseapp.com/projects/8994/tickets/305-size-reports-incorrectly-for-collections-when-more-than-one-item-is-added-with-build

Ah, awesome. Sorry for the false alarm!

Fred