问题描述:

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.

网友答案:

The delegation described in the answer from Sean worked perfectly, but I wanted something more generic as I'm going to have quite a few "Animals" and didn't want to have to update the delegate line every time I added a new column to BunnyAttr, etc. and I was trying to move as much code as I could up to the Animal class.

I then stumbled upon this blog posting and have decided to go the route of using method_missing in the Bunny class (eventually will define a version in the Animal class where I pass the attr class).

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

Would of course like comments on why this is a bad idea, if any.

相关阅读:
Top