Ruby on Rails中的多表继承vs单表继承

13

我在过去几个小时里一直在纠结应该采取哪种方法。我有一个通知模型。到目前为止,我使用notification_type列来管理类型,但我认为为通知的类型创建单独的类会更好,因为它们的行为不同。

现在,通知可以通过三种方式发送:SMS,Twitter,电子邮件

每个通知将具有:

id
subject
message
valediction
sent_people_count
deliver_by
geotarget
event_id
list_id
processed_at
deleted_at
created_at
updated_at

似乎STI是一个不错的选择,对吧?当然Twitter/SMS不会有主题,而Twitter也不会有发送人数和告辞语。在这种情况下,我认为它们共享大部分字段。但是,如果我为Twitter添加了一个"reply_to"字段和一个布尔值用于DM,怎么办?

我的观点是,现在使用STI很合理,但这是我以后可能会因为没有从MTI开始而后悔的案例吗?

更进一步,我想要一个Newsletter模型,它类似于通知,但不同之处在于它不使用event_id或deliver_by。

我可以看到所有通知子类都使用约2/3的通知基类字段。STI是否显而易见,还是应该使用MTI?


1
STI/MTI的权衡取舍的好总结:https://user3141592.medium.com/when-to-use-single-table-inheritance-vs-multiple-table-inheritance-db7e9733ae2e - Tim Abell
4个回答

21

你是否考虑过采用混合模型的方法?

使用单表继承来处理核心通知字段,然后将所有唯一的项目转移到特定的表/模型中,并在这些通知子类与其所属关系中使用belongs to/has one关系。

这需要更多的设置工作,但是一旦定义了所有的类和表,它就变得相当DRY。看起来是一种相当有效的存储方式。通过急切加载,不应该对数据库造成太大的额外负担。

为了举例说明,假设电子邮件没有独特的详细信息。这是如何映射出来的。

class Notification < ActiveRecord::Base
  # common methods/validations/associations
  ...

  def self.relate_to_details
    class_eval <<-EOF
      has_one :details, :class_name => "#{self.name}Detail"
      accepts_nested_attributes_for :details
      default_scope -> { includes(:details) }
    EOF
  end
end

class SMS < Notification
  relate_to_details

  # sms specific methods
  ...
end

class Twitter < Notification
  relate_to_details

  # twitter specific methods
  ...
end

class Email < Notification

  # email specific methods
  ...
end

class SMSDetail < ActiveRecord::Base
  belongs_to :SMS, :class_name => "SMS"           

  # sms specific validations
  ...
end

class TwiterDetail < ActiveRecord::Base
  belongs_to :twitter

  # twitter specific validations
  ...
end
每个详细表都将包含一个通知ID和仅形成通信需要但未包含在通知表中的列。虽然这会导致额外的方法调用以获取特定媒体的信息。
“这很好,但你认为这是必要的吗?”
在设计方面,只有极少数事情是必要的。随着CPU和存储空间成本的降低,必要的设计概念也会降低。我提出了这个方案,因为它提供了STI和MTI的最佳结合,并消除了它们的一些弱点。
就优势而言:
这种方案提供了STI的一致性。不需要重新创建表格。 链接表格避免了75%的行中存在大量空列的问题。您还可以轻松创建子类。如果新类型没有完全涵盖基本通知字段,则只需要创建匹配的详细信息表即可。它还使对所有通知的迭代简单易行。
从MTI中,您可以获得存储节省和满足类需求的定制化简易性,而无需为每种新通知类型重新定义相同的列。只需要定义唯一的列即可。
但是,这种方案也带有STI的主要缺陷。该表格将替换4张表。一旦它变得巨大,可能会开始导致减速。
简短的回答是,不,这种方法并非必要。我认为这是以最DRY的方式高效地处理问题的方式。在短期内,STI是解决它的方式。在很长一段时间内,MTI是前进的方式,但我们谈论的是您达到数百万通知的时候。这种方法是一种很好的中间地带,易于扩展。
详细的gem
我已经建立了一个基于您的解决方案的gem:https://github.com/czaks/detailed。使用它,您可以将Notification类简化为:
class Notification < ActiveRecord::Base
  include Detailed
end
其余的部分按照之前的方式处理。
额外的好处是,现在您可以直接访问(读取、写入、关联)子类特定的属性:`notification.phone_number`,而无需使用 `notification.details.phone_number`。您还可以在主类和子类中编写所有代码,使 Details 模型保持为空。您还将能够在大型数据集上执行较少的查询(在上面的示例中为 4,而不是 N+1),使用 `Notification.all_with_details` 而不是常规的 `Notification.all`。请注意,目前这个 gem 并没有经过很好的测试,尽管它适用于我的用例。

这很好知道,但你认为这是必要的吗? - Tony
感谢您的详细解释。我在网上没有找到任何有关实现混合模型的文章。我是否只需要创建一个通知表,然后为每种类型的详细信息创建一个详细信息表?非常感谢您的出色回答,希望我能给您更多的投票支持。最后一个问题:您说从长远来看,MTI 是正确的选择……那么为什么不一开始就设置呢?这是因为额外的详细信息调用最终会导致性能下降吗?我想这是速度和 DRY 的权衡。 - Tony
我认为Mike H关于坚持使用STI的想法可能是正确的。我的应用程序有一个主页面,用户可以查看所有类型的通知。如果选择MTI或混合模型,我需要进行更多的查询才能实现这一点。在标记答案正确之前,我将进行重构并确保STI是最好的选择,但无论如何,您提供的信息都非常有用。 - Tony
1
我尝试了这个实现,但是notification_id没有传递到详细模型。 - Damian
1
当然不是。我想我表述不太清楚。除非你在详细模型的belongs to语句中声明:foreign_key => :notification_id,否则Rails会认为notification_id列的名称与belongs_to关系中命名的关联名称相同。例如::belongs_to :twitter正在寻找一个twitter_id列,而不是notification_id列。 - EmFi
显示剩余2条评论

3

根据有限的信息,我建议使用STI。

关键问题是:在您的应用程序中是否有需要考虑所有类型通知的地方?如果是这样,那么这是坚持使用STI的强烈信号。


STI看起来更简单。有些地方我需要遍历所有通知并根据类型执行操作。 - Tony

2
我知道这个问题已经存在一段时间了,但是在找到解决方案之后,我认为这个答案可以在很多地方使用!最近,我派生了一个有前途的项目,用于在Rails中实现多表继承和类继承。我花了几天时间对其进行了快速的开发、修复、评论和文档化,并重新发布了它,作为用于Rails的CITIER类继承和表继承嵌入。
我认为你只需要构建模型,其中Twitter、Email和SMS从通知(Notification)继承。然后,在仅包括常见属性的Notifications迁移中,为三个子类型分别包含它们的唯一属性。
甚至可以在根通知类中定义一个函数,并在子类中重载它以返回不同的内容。
考虑看看它:https://github.com/PeterHamilton/citier。我发现它非常有用!顺便说一句,我欢迎社区在问题和测试、代码清理等方面提供任何帮助!我知道这是很多人会感激的事情。
但请务必定期更新,因为像我说的那样,它正在变得越来越好/进化。

1
has_one :details, :class_name => "#{self.class.name}Detail"

无法工作。在类定义的上下文中,self.class.name 是 'Class',因此 :class_name 总是 'ClassDetail'

所以必须是:

has_one :details, :class_name => "#{self.name}Detail"

但是非常好的想法!


直到我提出修改之前,我甚至都没注意到这个答案,但现在答案已经被改变了。 :) - Waynn Lue

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