动态实例化Rails嵌套的STI子类?

时间:2010-11-04 18:32:49

标签: ruby-on-rails dynamic types sti

假设我有一个类:

class Basket < ActiveRecord::Base
  has_many :fruits

“fruits”是一个STI基类,有“苹果”,“橙子”等子类......

我希望能在篮子中设置一个setter方法,如:

def fruits=(params)
  unless params.nil?
    params.each_pair do |fruit_type, fruit_data|
      fruit_type.build(fruit_data)
    end
  end
end

但是,显然,我得到了一个例外:

NoMethodError (undefined method `build' for "apples":String)

我认为的解决方法是这样的:

def fruits=(params)
  unless params.nil?
    params.each_pair do |fruit_type, fruit_data|
      "#{fruit_type}".create(fruit_data.merge({:basket_id => self.id}))
    end
  end
end

但是这会导致Fruit STI对象在Basket类之前被实例化,因此basket_id键永远不会保存在Fruit子类中(因为basket_id还不存在)。

我完全难过了。有人有什么想法吗?

2 个答案:

答案 0 :(得分:1)

不要在Basket中添加setter方法,而是将它添加到Fruit:

class Fruit < ActiveRecord::Base
  def type_setter=(type_name)
    self[:type]=type_name
  end
end

现在,您可以在通过关联构建对象时传递类型:

b = Basket.new
b.fruits.build(:type_setter=>"Apple")

请注意,您无法以这种方式分配:type,因为它受到质量分配的保护。

修改

哦,你想根据子类运行不同的回调?右。

你可以这样做:

fruit_type = "apples"
b = Basket.new
new_fruit = b.fruits << fruit_type.titleize.singularize.constantize.new
new_fruit.class # Apple

或为每种类型定义has_many关联:

require_dependency 'fruit' # assuming Apple is defined in app/models/fruit.rb

class Basket
  has_many :apples
end

然后

fruit_type = "apples"
b = Basket.new
new_fruit = b.send(fruit_type).build
new_fruit.class # Apple

答案 1 :(得分:-1)

在Ruby术语中,"#{x}"简单地等同于x.to_s,其中String值与字符串本身完全相同。在其他语言中,如PHP,您可以取消引用字符串并将其视为一个类,但这不是这种情况。你可能的意思是:

fruit_class = fruit_type.titleize.singularize.constantize
fruit_class.create(...)

constantize方法从字符串转换为等效类,但它区分大小写。

请注意,您可能会有人可能会创建fruit_type设置为"users"的内容,然后继续创建管理员帐户。或许更负责任的是做一个额外的检查,你所做的是实际上是正确的班级。

fruit_class = fruit_type.titleize.singularize.constantize
if (fruit_class.superclass == Fruit)
  fruit_class.create(...)
else
  render(:text => "What you're doing is fruitless.")
end

以这种方式加载类时需要注意的一点是constantize不会自动加载类,比如在应用程序中拼写它们。在开发模式下,您可能无法创建尚未显式引用的子类。您可以通过使用映射表来解决潜在的安全问题并一次性预加载,从而避免这种情况:

fruit_class = Fruit::SUBCLASS_FOR[fruit_type]

您可以像这样定义此常量:

class Fruit < ActiveRecord::Base
  SUBCLASS_FOR = {
    'apples' => Apple,
    'bananas' => Banana,
    # ...
    'zuchini' => Zuchini
  }
end

在模型中使用文字类常量会立即加载它们。