Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

validates_presence_of causes after_initialize to be called with a weird self

I've had this model which was working fine:

class Weight < ActiveRecord::Base
  belongs_to :user
  validates_presence_of :weight, :measured_on
  attr_accessible :weight, :measured_on

  def after_initialize
    self.measured_on ||= Date.today
  end
end

I added it this line

validates_uniqueness_of :measured_on, :scope => :user_id

and it started throwing an error on validation. Not a validation error but a Ruby error:

>> w.valid?
ActiveRecord::MissingAttributeError: missing attribute: measured_on
    from /Users/pupeno/Projects/sano/app/models/weight.rb:8:in `after_initialize'

I've put a debugger statement in after_initialize and I've noticed something unexpected. When I create a new weight it works as expected and the self object on after_initialize is the expected weight:

>> w = Weight.new
/Users/pupeno/Projects/sano/app/models/weight.rb:9
self.measured_on ||= Date.today
(rdb:1) p self
#<Weight id: nil, user_id: nil, weight: nil, measured_on: nil, created_at: nil, updated_at: nil>
(rdb:1) c
=> #<Weight id: nil, user_id: nil, weight: nil, measured_on: "2009-11-22", created_at: nil, updated_at: nil>

When I run w.valid? it gets weird. after_initialize is called again, I'm not sure why, and the self object is nothing I expected:

>> w.valid?
/Users/pupeno/Projects/sano/app/models/weight.rb:9
self.measured_on ||= Date.today
(rdb:1) p self
#<Weight id: 1>
(rdb:1) p self.inspect
"#<Weight id: 1>"
(rdb:1) p self.class
Weight(id: integer, user_id: integer, weight: float, measured_on: date, created_at: datetime, updated_at: datetime)
(rdb:1) p self.measured_on
ActiveRecord::MissingAttributeError Exception: missing attribute: measured_on
(rdb:1)

It seems like another Weight object was created without any attributes but the id set. Any ideas why? Is this a bug or the expected behavior? Am I doing something wrong by setting the measured_on on after_initialize?

My current workaround, in case anybody is having the same problem, is

class Weight < ActiveRecord::Base
  belongs_to :user
  validates_presence_of :weight, :measured_on
  validates_uniqueness_of :measured_on, :scope => :user_id
  attr_accessible :weight, :measured_on

  def after_initialize
    if self.has_attribute? :measured_on
      self.measured_on ||= Date.today
    end
  end
end

but I'd like to have a proper solution.

like image 502
pupeno Avatar asked Nov 22 '09 10:11

pupeno


1 Answers

I think you're hitting a rails bug I recently battled with. See This blog entry linking to the related lighthouse bug.

My understanding is that what's happening is that some prior piece of rails code does a "select id from tablename" to see if an entry exists or matches. The object then caches that the only field that exists for the table is "id". Your code then runs, and the "attributes" value is then incorrect, reflecting only the id field.

From what I could find, this only happened when this particular code path was hit, and didn't generally upset things, except when doing validations.

What I did to get around it was wrap the after_initialise code in a begin/rescue ActiveRecord::MissingAttributeError block. Then I wrote a big note in the application and above each item indicating when a new version of rails is released, we could remove this.

Yes, I'm sure there are more elegant solutions.

def after_initialize
  begin
    # ... updates here
    # self.unique_reference = UUIDTools::UUID.random_create.to_s
  rescue ActiveRecord::MissingAttributeError
  end
end

Or you could also do:

def after_initialize
  if self.has_attribute? :measured_on
    self.measured_on ||= Date.today
  end
end
like image 102
oskarpearson Avatar answered Nov 14 '22 22:11

oskarpearson