Rails模型子类化 -> 多表继承或多态性

12

这是我的设置,接下来是我试图实现的解释。

class Layer < ActiveRecord::Base
  has_and_belongs_to_many :components
end

class Component < ActiveRecord::Base
  has_and_belongs_to_many :layers 
end

class ImageComponent < Component
  # I want this table to inherit from the Component table
  # I should be able to add image-specific fields to this table
end

class VideoComponent < Component
  # I want this table to inherit from the Component table
  # I should be able to add video-specific fields to this table
end

我希望能够做到以下几点:

layer.components << ImageComponent.create
layer.components << VideoComponent.create

在实践中,我意识到ImageComponentVideoComponent实际上必须从ActiveRecord::Base继承。在Rails中有没有优雅的实现模型子类化的方式?

目前,我的Component模型被设置为polymorphic,以便ImageComponentVideoComponent分别具有has_one :component, as: :componentable。尽管如此,这会给我的代码增加一层烦恼和丑陋:

image_component = ImageComponent.create
component = Component.create
component.componentable = image_component
layer.components << component

我想简单解释一下,我希望在层和组件之间实现一种habtm关系。我有多种类型的组件(例如ImageComponent、VideoComponent),它们都具有相同的基本结构,但与它们相关联的字段不同。有没有什么建议可以实现这个目标?我感觉自己漏掉了什么,因为我的代码感觉很笨拙。


3
我认为继承本身应该能够发挥作用(也就是说,“create”应该是“ImageComponent”的一个方法),但 AR(Active Record)的相关内容不会起作用,因为它基于类名对文件和表名称等做出了一些假设。虽然我认为大部分Rails框架的自动化操作都可以被覆盖,但也许更好的方法是使用mixin而不是直接继承,以保持DRY和清晰。 - Tom Harrison
1
问题到底是什么?Rails实现了模型子类化。这一切与多态性与多表继承有什么关系? - phoet
2
多态性和多表继承是我尝试做的两种方法,但对于我的情况,这两种方法都不特别干净(DRY)。我想将多表继承与模型子类化相结合。据我所知,您无法创建一个模型的子类,该子类给子类提供一个新表。我在上面的示例中拥有的子类无法实现它们自己的字段 - 它们受限于超类的字段。 - Michael Frederick
3个回答

10

2
谢谢。这是我的原始实现,但感觉很混乱,子类特定的字段对所有子类都可用。例如,如果我想仅向ImageComponent添加一个字段,则该字段也可用于VideoComponent。唉。 - Michael Frederick
3
虽然字段是可访问的,但每个子类可以有不同的验证。因此,对于“VideoComponent”,您可以明确指定某些字段应为空白。但我可以想象,“ImageComponent”和“VideoComponent”之间存在很多可能的重叠,所以在这种情况下,STI似乎是最好的解决方案。如果您真的想要简化数据模型,可以使用一个“Properties”表,其中每个组件都有许多个属性(has_many :properties)。希望有所帮助。 - nathanvda

2
这里的主要问题在于组件及其类型,而不是层和组件。我曾遇到类似的问题。会针对你的问题解释解决方案。
将类型(图像/视频)作为组件的资源存储,并为组件而非所有类型设置控制器。
让模型结构如下:
Component < ActiveRecord::Base
  accepts_nested_attributes_for :resource
  belongs_to :resource, :polymorphic => true, :dependent => :destroy

  def resource_attributes=(params = {})
    self.resource = spec_type.constantize.new unless self.resource
    self.resource.attributes = params.select{|k| self.resource.attribute_names.include?(k) || self.resource.class::ACCESSOR.include?(k.to_sym)}
  end

#component will be either image or video and not both

Image < ActiveRecord::Base
  has_one :component, as :resource 

Video < ActiveRecord::Base
  has_one :component, as :resource

将单个控制器 ComponentsController 用于 CRUD 组件。由于组件接受资源属性(例如图片/视频),因此您可以保存组件以及资源,并为每个资源添加普通验证。

添加组件的基本视图如下:

= form_for(@component, :url => components_path, :method => :post) do |f|
  = fields of Component
  = f.fields_for :resource, build_resource('image') do |image|
    = fields of resource Image
  = f.fields_for :resource, build_resource('video') do |video|
    = fields of resource Video

使用辅助方法可以添加图像/视频字段

module ComponentsHelper
  def build_resource(klass)
    klass  = "{klass.capitalize}"
    object = eval("#{klass}.new")
    if @component.resource.class.name == klass
      object = @component.resource
    end
    return object
  end
end

因为组件只能有一个相关资源(图片/视频),所以您需要在视图上选择相关资源类型(在我的情况下,它是一个下拉列表),并根据选定的资源显示其字段,并隐藏/删除所有其他资源字段(如果选择了图像,则使用JavaScript删除视频字段)。当表单被提交时,Component模型中的方法会过滤出所有预期资源的键值对,并创建组件及其相关资源。

另外:

1)确保每个资源的字段名称唯一,因为当表单被提交时,隐藏的资源(不需要的资源)字段也会被提交,这会覆盖预期资源的字段。

2)上述模型结构对于仅限资源attr_accessor存在问题(它们不能在Rails控制台上访问)。可以通过以下方式解决:

ACCESSOR = ['accessor1', 'accessor2'] #needed accessors
has_one :component, :as => :resource
attr_accessor *ACCESSOR

请查看如何实现具有3个固定类别的职位发布功能

希望这能帮到您。


1

使用STI,您正在与多个模型类共享同一张表,因此如果您希望子类模型具有唯一字段(数据库列),则需要在该公共表中表示它们。从您示例中的注释中可以看出,这正是您想要的。

然而,有一个技巧,涉及到在表中拥有一个字符串列,每个模型都可以使用它来存储自定义序列化数据。为了做到这一点,必须允许这些数据元素不被索引,因为您将无法在SQL中轻松搜索它们。假设您将其称为aux字段。将其放入父模型中:

  require 'ostruct'
  serialize :aux, OpenStruct

现在假设你想在一个子类模型中使用名为managerexperience的字段,但其他STI模型不需要这些字段,而且你也不需要根据这些属性进行搜索。因此,你可以在子类模型中这样做:

# gets the value
def manager
  return self.aux.manager
end

# sets the value
def manager=(value)
  self.aux.manager = value
end

 # gets the value
def experience
  return self.aux.experience
end

# sets the value
def experience=(value)
  self.aux.experience = value
end

在这个例子中,单表继承仍然可以正常工作,并且您还可以为子类模型获取自定义持久属性。这使您能够在多个模型之间共享代码和数据库资源的好处,但也允许每个模型具有独特的属性。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接