开发者

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: floa开发者_StackOverflow社区t, 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.


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


This problem is related to the rails ticket #3165. Read up there, if you're interested in the how and why this is occuring

I just spent half a day on this, before I finally found that ticket. While I'm sad that it's been almost exactly a year since this was reported, and it hasn't been fixed yet, here's my simple work around for my scenario:

validates_uniqueness_of :email, :scope => :library_id

def after_initialize
  self.status ||= "Invited"
end

This will cause a 'MissingAttributeError' to be thrown, if there are records returned by the validates_uniqueness_of query. My simple solution is this:

def after_initialize
  self.status ||= "Invited" if new_record?
end

While other people are having more complex issues, this should solve the simple case, until an actual solution is commited into rails.


validates_uniqueness_of needs to scan your database for entries. ActiveRecord is loading all of those other entries as instances of your model. But to cut down on processor/memory use it it's not adding methods for the attributes, because it shouldn't need them for a quick existence check. However it's still instantiating all existing records as instances of your model so after_initialize is called.

The work around is to modify the @attributes hash directly instead of relying on accessors:

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
    @attributes["measured_on"] ||= Date.today
  end
end
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜