Archive for November, 2007

Rails Bug With Has Many Collections Empty? and Build

Thursday, November 8th, 2007

After my long rant about problems in Ruby on Rails has_many collections and empty? calls, we spent some more time and really condensed the problem. I can explain this problem much more clearly now.

Let’s take the following code:


f = Foo.new
f.bars.build
f.bars.empty?
pp f.bars

What do you think will be printed out when we pp f.bars?

Well, if you’re like me, you’d think it would return an array with one Bar in it. And, if you were like me, you’d be wrong.

Turns out that the call to f.bars.empty? doesn’t realize you’ve ever added a new Bar to the collection of bars. Therefore, it will go into the database and do a select count(*) to determine the size of the collection. It will return zero, because at this point you haven’t saved anything into the database. Not only is going to the database to determine the size of the collection wrong (in the line above, I’ve added a Bar though build so empty? should know there’s at least one), but empty? in the process is caching the empty collection!

Because of this caching, our last line of code here is printing out an empty array. This effectively deletes the original Bar we added via build.

This has to be a bug. In fact, I’m filing a bug at Rails’ Trac system.

How do you work around this? Just change your usage of empty? to length.zero?:


f = Foo.new
f.bars.build
f.bars.length.zero?
pp f.bars

The above code will work just fine!

Many thanks to Wenyi for researching this with me.

links for 2007-11-07

Tuesday, November 6th, 2007

When Empty Isn’t Empty With Rails Active Record Collections

Sunday, November 4th, 2007

So I’m writing some Rails code, just playing around, and I run into a very strange situation. I’ve found a situation where Rails’ Active Record code is calculating the results to empty? incorrectly, even if I add elements to the collection. So I had a has_many collection with elements inside of it, but calls to length or empty were returning 0 and true.

Here’s my simple models:


class Query < ActiveRecord::Base
  has_many :measures
  validates_presence_of :cube_name

  def measure_attributes=(attrs)
    attrs.each {|a| measures.build(a)}
  end
end

and


class Measure < ActiveRecord::Base
  belongs_to :query
  acts_as_list :scope => :query
  has_one :condition, :as => :conditionable
end

Notice that in Query, I’ve added a measure_attributes method which automatically handles the construction of new Measure instances from the form parameters.

Here’s the simple nested form. From the models, a Query has many Measures:



<% form_for :query, :url => {:action => :new} do |f| %>
	Cube Name: <%= f.text_field :cube_name %>

	<% @query.measures.each do |measure| %>
		<div>
		<% fields_for "query[measure_attributes][]", measure do |mf| %>
			Measure Name: <%= mf.text_field :name %>
		<% end %>
		</div>
	<% end %>

	<%= submit_tag %>
<% end %>

Here’s what I was doing in my controller. Again, very simple:


class QueriesController < ApplicationController
  def new
    @query = Query.new(params[:query])
    @query.measures.build if @query.measures.empty?
  end
end

Now, I wouldn’t normally have the new method handle it this way (I would have both a create and a new), but this was test code. My check against empty? was basically saying “if this is the first time I’ve hit this method, throw in a blank Measure so the form will show at least show one Measure.”

Well, it turns out, that call to empty? is calculated by checking the database! Here’s the actual query:


SELECT count(*) AS count_all FROM measures WHERE (measures.query_id = NULL)

Even though I added Measures via measure_attributes, ActiveRecord here doesn’t seem to acknowledge that at this point.

When I rendered the new template after the form is submitted, the loop in the RHTML (@query.measures.each) doesn’t iterate, because it thinks there’s no measures.

Sigh.

So, here’s what we learned. Don’t call length or empty? on an Active Record managed has_many collection unless you understand how it’s calculating the size of the contents (via the database). (Also, I still don’t quite understand when it goes to the database and when it uses the actual items I’ve manually added.)

Here’s the best way to handle this situation, completely avoiding the call to empty?:


class QueriesController < ApplicationController
  def new
    @query = Query.new(params[:query])
    # add one so the form will have something to render in the beginning
    @query.measures.build
  end

  def create
    @query = Query.new(params[:query])
    render :action => ‘new’
  end
end

links for 2007-11-03

Friday, November 2nd, 2007