经过多次反复试验并寻找现有答案后,似乎存在一个根本性的误解,我希望得到一些澄清和/或指导。
提前注意:我正在使用多个表继承并且有充分的理由这样做,因此无需将我引回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',用于#&lt; Bunny:0x0 ..&gt;
为什么会这样,如何进行第二次调用以返回值?
答案 0 :(得分:1)
尝试将初始化时当前的代码移动到after_initialize
回调。
after_initialize do
# the code above...
end
当ActiveRecord从数据库加载时,它实际上并没有调用initialize。当您致电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
创建getter和setter,并且它也会为您跟踪其脏状态,允许您在保存时bunny_attr
在bunny
上调用保存。如果allow_nil
为bunny_attr
,则将nil
设置为false会导致ActiveRecord抛出错误。
答案 1 :(得分:0)
Sean在回答中所描述的代表工作得非常完美,但我想要更通用的东西,因为我会有很多&#34;动物&#34;并且我不想每次向BunnyAttr等添加新列时都必须更新委托行。我试图将尽可能多的代码移动到Animal类。
然后我偶然发现了this blog posting并决定在Bunny类中使用method_missing的路径(最终将在Animal类中定义一个我传递attr类的版本)。
def method_missing(method_name, *args, &block)
bunny_attr.respond_to?(method_name) ?
bunny_attr.send(method_name, *args) :
super
end
当然会喜欢评论为什么这是一个坏主意,如果有的话。