Rails 3 after_initialize не работает, когда create вызывается с блоком

Я пытаюсь установить некоторые значения по умолчанию для объекта с помощью after_initialize. Проблема, с которой я сталкиваюсь, заключается в том, что я бы хотел, чтобы это вызывалось независимо от того, как создается объект.

Мой класс:

class Foo < ActiveRecord::Base

  serialize :data

  after_initialize :init

  def init
    self.data ||= {}
    self.bar ||= "bar"
    self.baz ||= "baz"
  end

end

Все работает нормально, если я вызываю Foo.new, Foo.new(:bar => "things") и Foo.create(:baz => 'stuff'). Однако, когда я использую блок с create, обратный вызов after_initialize не запускается.

obj = Foo.create do |f|
  f.bar = "words"
  f.data = { :attr_1 => 1, :attr_2 => 2 }
end

Это просто возвращает obj.baz = > nil вместо "baz", если другие атрибуты установлены правильно.

Я пропустил что-то с тем, как выполняются обратные вызовы, с различиями с вызовом create с блоком и без или по умолчанию значения, сбитые блоком?

UPDATE

Обнаружена проблема.

Оказывается, что вызов create с блоком и без, является немного отличающимся. Когда вы вызываете create без блока и просто передаете хэш параметров, для всех целей и задач вы вызываете Foo.new({<hash of argument>}).save, а обратный вызов after_initialize запускается непосредственно перед сохранением, как вы ожидали.

Когда вы вызываете create с блоком, происходит что-то немного другое. Порядок событий Foo.new вызывается с любыми аргументами, которые вы передаете, затем вызывается after_initialize, затем блок запускается. Поэтому, если вы используете блок (как я был) взаимозаменяемо с параметрами хэша, чтобы сделать вещи немного более удобочитаемыми, вы можете получить бит, потому что ваш after_initialize запускается до того, как все параметры, которые вы намереваетесь установить, действительно установлены.

Я получил бит, потому что я делал дополнительную работу в after_initialize, чтобы установить дополнительные атрибуты, основанные на значении того, что передавалось. Поскольку ничего не было установлено при вызове after_initialize, ничего не установлено правильно, и мои проверки не удались.

Мне пришлось сделать вызовы init. Один раз на after_initialize и один раз на before_validation. Не самый чистый, но он решил проблему.

Спасибо, Брэндон за то, что указал мне в правильном направлении.

Ответ 1

Я не могу воспроизвести это. У меня есть приложение, удобное со следующим (упрощенным) классом:

class Service < ActiveRecord::Base
  serialize        :data, Hash
  after_initialize :create_default_data
  attr_accessible  :data, :token

  protected

    def create_default_data
      self.data ||= Hash.new
    end
end

Здесь сеанс IRB:

ruby-1.9.2-p136 :001 > obj = Service.create do |s|
ruby-1.9.2-p136 :002 >     s.token = "abc"
ruby-1.9.2-p136 :003?>   end
 => #<Service id: 22, user_id: nil, type: nil, data: {}, created_at: "2011-03-05 04:18:00", updated_at: "2011-03-05 04:18:00", token: "abc"> 
ruby-1.9.2-p136 :004 > obj.data
 => {}

Как видите, data как инициализирован методом after_initialize на пустой хеш. Код Rails указывает, что это имеет смысл; в create:

def create(attributes = nil, &block)
  if attributes.is_a?(Array)
    attributes.collect { |attr| create(attr, &block) }
  else
    object = new(attributes)
    yield(object) if block_given?
    object.save
    object
  end
end

Итак create вызывает new и присваивает значение object перед ним yield s. Здесь соответствующая часть в new:

def initialize(attributes = nil)
  # truncated for space
  result = yield self if block_given?
  run_callbacks :initialize
  result
end

Как вы можете видеть, new безоговорочно вызывает обратные вызовы initialize до того, как он вернется, и, таким образом, до того, как create даже выйдет на передаваемый вами блок. К тому времени, когда ваш блок получает объект, метод after_initialize уже выполнил.

Дважды проверьте, что (1) ваша версия Rails обновлена ​​(3.0.5 на данный момент я считаю) и что (2) ничто не устанавливает baz, если вы не осознаете это.