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

发布于 2024-10-01 08:51:31 字数 814 浏览 3 评论 0原文

假设我有一个这样的类:

class Basket < ActiveRecord::Base
  has_many :fruits

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

我希望能够在 Basket 中有一个 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 类之前实例化,因此篮子 id 键永远不会保存在 Fruit 子类中(因为篮子 id 不保存)还存在)。

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

Let's say I have a class like:

class Basket < ActiveRecord::Base
  has_many :fruits

Where "fruits" is an STI base class having subclasses like "apples", "oranges", etc...

I'd like to be able to have a setter method in Basket like:

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

But, obviously, I get an exception like:

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

A workaround I thought of works like this:

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

But that causes the Fruit STI object to be instantiated before the Basket class, and so the basket_id key is never saved in the Fruit subclass (because basket_id doesn't exist yet).

I'm totally stumped. Anyone have any ideas?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

望她远 2024-10-08 08:51:31

不要在 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

Instead of adding a setter method in Basket, add it in Fruit:

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

Now you can pass the type in when you build the object through an association:

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

Note that you can't assign :type this way, since it is protected from mass assignment.

EDIT

Oh, you wanted to run different callbacks depending on the subclass? Right.

You could do this:

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

or define a has_many association for each type:

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

class Basket
  has_many :apples
end

then

fruit_type = "apples"
b = Basket.new
new_fruit = b.send(fruit_type).build
new_fruit.class # Apple
瘫痪情歌 2024-10-08 08:51:31

在 Ruby 术语中,"#{x}" 简单地等同于 x.to_s,对于字符串值来说,它与字符串本身完全相同。在其他语言(例如 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

在模型中使用文字类常量将具有立即加载它们的效果。

In Ruby terms, "#{x}" is simply equivalent to x.to_s which for String values is exactly the same as the string itself. In other languages, like PHP, you can de-reference a string and treat it as a class, but that's not the case here. What you probably mean is this:

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

The constantize method converts from a string to the equivalent class, but it is case sensitive.

Keep in mind that you're exposing yourself to the possibility someone might create something with fruit_type set to "users" and then go ahead and make an administrator account. What's perhaps more responsible is to do an additional check that what you're making is actually of the right class.

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

One thing to watch out for when loading classes this way is that constantize will not auto-load classes like having them spelled out in your application does. In development mode you may be unable to create subclasses that have not been explicitly referenced. You can avoid this by using a mapping table which solves the potential security problem and pre-loading all at once:

fruit_class = Fruit::SUBCLASS_FOR[fruit_type]

You can define this constant like this:

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

Using the literal class constant in your model will have the effect of loading them immediately.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文