1

经过多次反复试验并寻找现有答案后,我似乎有一个基本的误解,并且希望得到一些澄清和/或指导。

提前注意:我正在使用多表继承,并且有充分的理由这样做,所以不需要引导我回到 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 将返回“#<Bunny:0x0..> 的未定义方法 'ear_length'

为什么会这样以及如何获得第二次调用以返回值?

4

2 回答 2

1

尝试将您当前在初始化中的代码移动到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这将自动为and创建 getter 和 setter hop_style,它也会为您跟踪它们的脏状态,允许您bunny_attr在调用 save on 时进行保存bunny。如果is设置allow_nil为 false 将导致 ActiveRecord 抛出错误。bunny_attrnil

于 2014-07-01T02:08:25.447 回答
0

肖恩的回答中描述的代表团工作得很好,但我想要一些更通用的东西,因为我将有很多“动物”,并且不想每次我添加一个新列时都更新代理行BunnyAttr 等,我试图将尽可能多的代码移至 Animal 类。

然后我偶然发现了这篇博文,并决定在 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

当然,如果有的话,我想评论一下为什么这是一个坏主意。

于 2014-07-02T00:47:36.423 回答