Неопределенный метод в некоторых случаях для метода экземпляра, определенного в подклассе
После долгих проб и ошибок и поиска существующего ответа у меня возникло фундаментальное недоразумение, и мне бы хотелось получить некоторые разъяснения и / или указания.
Обратите внимание: я использую наследование нескольких таблиц и у меня для этого есть веские причины, поэтому нет необходимости возвращать меня в 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.first
ActiveRecord в конечном итоге вызывает следующий метод:
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
Конечно, хотелось бы комментарии о том, почему это плохая идея, если таковые имеются.