Неопределенный метод в некоторых случаях для метода экземпляра, определенного в подклассе

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

Обратите внимание: я использую наследование нескольких таблиц и у меня для этого есть веские причины, поэтому нет необходимости возвращать меня в STI:)

У меня есть базовая модель:

class Animal < ActiveRecord::Base
  def initialize(*args)
    if self.class == Animal
      raise "Animal cannot be instantiated directly"
    end
    super
  end
end

И подкласс:

class Bunny < Animal
  has_one(:bunny_attr)

  def initialize(*args)
    attrs = args[0].extract!(:ear_length, :hop_style)

    super

    self.bunny_attr = BunnyAttr.create!

    bunny_attrs_accessors 

    attrs.each do |key, value|
      self.send("#{key}=", value)
    end

  def bunny_attrs_accessors
    attrs = [:ear_length, :hop_style]

    attrs.each do |att|
      define_singleton_method att do
        bunny_attr.send(att)
      end

      define_singleton_method "#{att}=" do |val|
        bunny_attr.send("#{att}=", val)
        bunny_attr.save!
      end
    end
  end
end

И связанный набор данных

class BunnyAttr < ActiveRecord::Base
  belongs_to :bunny
end

Если я тогда сделаю что-то вроде этого:

bunny = Bunny.create!(name: "Foofoo", color: white, ear_length: 10, hop_style: "normal")
bunny.ear_length
Bunny.first.ear_length

bunny.ear_length вернет "10", а Bunny.first.ear_length вернет "неопределенный метод ear_length" для #

Почему это так и как мне получить второй вызов для возврата значения?

2 ответа

Решение

Попробуйте переместить код, который у вас есть в инициализации, в after_initialize Перезвоните.

after_initialize do
  # the code above...
end

Когда ActiveRecord загружается из базы данных, он фактически не вызывает инициализацию. Когда вы звоните Bunny.firstActiveRecord в конечном итоге вызывает следующий метод:

def find_by_sql(sql, binds = [])
  result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds)
  column_types = {}

  if result_set.respond_to? :column_types
    column_types = result_set.column_types
  else
    ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`"
  end

  result_set.map { |record| instantiate(record, column_types) }
end

И метод экземпляра выглядит так:

 def instantiate(record, column_types = {})
    klass = discriminate_class_for_record(record)
    column_types = klass.decorate_columns(column_types.dup)
    klass.allocate.init_with('attributes' => record, 'column_types' => column_types)
  end

А также init_with...

def init_with(coder)
  @attributes   = self.class.initialize_attributes(coder['attributes'])
  @column_types_override = coder['column_types']
  @column_types = self.class.column_types

  init_internals

  @new_record = false

  run_callbacks :find
  run_callbacks :initialize

  self
end

init_internals просто устанавливает некоторые внутренние переменные, такие как @readonly, @new_recordи тд #initialize никогда не вызывается при загрузке записей из базы данных. Вы также заметите run_callbacks :initialize который работает, когда вы загружаете из базы данных.

Обратите внимание, что приведенный выше код извлечен из Rails 4.1.1, но большая часть процесса инициализации должна быть такой же для других последних версий Rails.

Изменить: я просто думал об этом немного больше, и вы можете удалить код, где вы определяете методы setter, а затем вызывать их, если вы делегируете методы BunnyAttr,

class Bunny < Animal
  has_one :bunny_attr
  delegate :ear_length, :hop_style, to: :bunny_attr, prefix: false, allow_nil: false
end

Это автоматически создаст геттеры и сеттеры для ear_length а также hop_style, и он будет отслеживать их грязный статус для вас, что позволяет вам сохранить bunny_attr когда вы звоните сохранить bunny, настройка allow_nil ложь заставит ActiveRecord выдать ошибку, если bunny_attr является nil,

Делегация, описанная в ответе Шона, сработала отлично, но я хотел что-то более общее, поскольку у меня будет довольно много "животных", и мне не нужно будет обновлять строку делегата каждый раз, когда я добавляю новый столбец в BunnyAttr и т. Д., И я пытался переместить как можно больше кода в класс Animal.

Затем я наткнулся на это сообщение в блоге и решил пойти по пути использования method_missing в классе Bunny (в конце концов я определю версию в классе Animal, где я пропущу класс attr).

def method_missing(method_name, *args, &block)
  bunny_attr.respond_to?(method_name) ?
  bunny_attr.send(method_name, *args) :
  super
end

Конечно, хотелось бы комментарии о том, почему это плохая идея, если таковые имеются.

Другие вопросы по тегам