After much trial-and-error and searching for an existing answer, there seems to be a fundamental misunderstanding I'm having and would love some clarification and/or direction.
Note in advance: I'm using multiple table inheritance and have good reasons for doing so, so no need to direct me back to STI :)
I have a base model:
class Animal < ActiveRecord::Base
def initialize(*args)
if self.class == Animal
raise "Animal cannot be instantiated directly"
end
super
end
end
And a sub-class:
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
And a related set of data
class BunnyAttr < ActiveRecord::Base
belongs_to :bunny
end
If I then do something like this:
bunny = Bunny.create!(name: "Foofoo", color: white, ear_length: 10, hop_style: "normal")
bunny.ear_length
Bunny.first.ear_length
bunny.ear_length will return "10", while Bunny.first.ear_length will return "undefined method 'ear_length' for #<Bunny:0x0..>
Why is that and how do I get the second call to return a value?
Try moving the code you currently have in initialize to an after_initialize
callback.
after_initialize do
# the code above...
end
When ActiveRecord loads from the database, it doesn't actually call initialize. When you call Bunny.first
, ActiveRecord eventually calls the following method:
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
And the instantiate method looks like this:
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
And 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
just sets some internal variables, like @readonly
, @new_record
, etc, so #initialize
never actually gets called when you load records from the database. You'll also notice the run_callbacks :initialize
that does run when you load from the db.
Note the above code is extracted from Rails 4.1.1, but much of the initialization process should be the same for other, recent versions of Rails.
Edit: I was just thinking about this a little more, and you can remove the code where you define the setter methods and then call them if you delegate the methods to BunnyAttr
.
class Bunny < Animal
has_one :bunny_attr
delegate :ear_length, :hop_style, to: :bunny_attr, prefix: false, allow_nil: false
end
This will automatically create the getters and setters for ear_length
and hop_style
, and it'll track their dirty status for you, too, allowing you to save bunny_attr
when you call save on bunny
. Setting allow_nil
to false will cause ActiveRecord to throw an error if bunny_attr
is nil
.