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

31 views
Skip to first unread message

Johannes Fahrenkrug

unread,
Aug 19, 2008, 3:51:46 AM8/19/08
to Ruby on Rails: Core
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

--
http://blog.springenwerk.com

Ryan Bigg

unread,
Aug 19, 2008, 4:00:36 AM8/19/08
to rubyonra...@googlegroups.com
It would be loading the associated objects without checking to see if
they are a #new_record?... seems like a bug to me.

Johannes Fahrenkrug

unread,
Aug 19, 2008, 5:24:57 AM8/19/08
to Ruby on Rails: Core
Should I open a ticket for this?

Xavier Noria

unread,
Aug 19, 2008, 11:10:31 AM8/19/08
to rubyonra...@googlegroups.com
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.

Xavier Noria

unread,
Aug 19, 2008, 11:23:42 AM8/19/08
to rubyonra...@googlegroups.com
On Tue, Aug 19, 2008 at 5:10 PM, Xavier Noria <f...@hashref.com> wrote:

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

Xavier Noria

unread,
Aug 19, 2008, 11:39:33 AM8/19/08
to rubyonra...@googlegroups.com
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.

Frederick Cheung

unread,
Aug 19, 2008, 11:47:22 AM8/19/08
to rubyonra...@googlegroups.com

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

Frederick Cheung

unread,
Aug 19, 2008, 11:58:41 AM8/19/08
to rubyonra...@googlegroups.com

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

Fred


Xavier Noria

unread,
Aug 19, 2008, 7:05:25 PM8/19/08
to rubyonra...@googlegroups.com

Xavier Noria

unread,
Aug 19, 2008, 7:39:15 PM8/19/08
to rubyonra...@googlegroups.com
On Tue, Aug 19, 2008 at 5:58 PM, Frederick Cheung
<frederic...@gmail.com> wrote:

> 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

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

Frederick Cheung

unread,
Aug 19, 2008, 7:44:12 PM8/19/08
to rubyonra...@googlegroups.com

On 20 Aug 2008, at 00:39, Xavier Noria wrote:

>
> On Tue, Aug 19, 2008 at 5:58 PM, Frederick Cheung
> <frederic...@gmail.com> wrote:
>
>> 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

Reply all
Reply to author
Forward
0 new messages