在设计 Rails 数据库模式时如何避免使用问题?

How do I avoid using Concerns when designing Rails database schemas?

我读了很多博客,经常遇到的主题之一是担忧(至少 Rails 定义它们的方式)正在损害软件。总的来说,我同意——简单地将行为包含到模型中违反了单一责任原则。你最终得到了一个做得太过分的上帝-class。

但是与从博客中收集的许多意见一样,很少提供替代架构。

让我们举一个示例应用程序,大致基于我必须维护的应用程序。它本质上是一个 CMS,许多 Rails 应用程序往往是。

目前每个模型都有大量的关注点。让我们在这里使用一些:

class Article < ActiveRecord::Base
  include Concerns::Commentable
  include Concerns::Flaggable
  include Concerns::Publishable
  include Concerns::Sluggable
  ...
end

您可以想象 'Commentable' 只需要在文章中添加少量代码。足以与评论对象建立关系并提供一些实用方法来访问它们。

可标记,允许用户标记不当内容,最终会向模型添加一些字段:例如 flagged, flagged_by, flagged_at。以及一些添加功能的代码。

Sluggable 添加了一个用于在 URL 中引用的 slug 字段。还有一些代码。

Publishable 添加了发布日期和状态字段,以及更多代码。

现在如果我们添加一种新的内容会怎样?

class Album < ActiveRecord::Base
  include Concerns::Flaggable
  include Concerns::Sluggable
  include Concerns::Publishable
  ...
end

相册有点不同。你不能评论它们,但你仍然可以发布它们并标记它们。

然后我再添加一些内容类型:比方说事件和个人资料。

我发现这个架构目前存在一些问题:

那么什么是更好的方法?

我看到装饰器被提升为在需要的地方添加功能的一种方式。这可能有助于解决包含代码的问题,但不一定会改进数据库结构。它还看起来不必要地繁琐,并且涉及向代码添加额外的循环来装饰模型数组。

到目前为止我的想法是这样的:

创建通用 'content' 模型(和 table):

class Content < ActiveRecord::Base
end

关联的 table 可能很小。它可能应该有某种类型的 'type' 字段,并且可能有一些对所有内容都通用的东西——比如 URL 的 type slug。

然后我们可以为每个行为创建一个关联模型,而不是添加关注点:

class Slug < ActiveRecord::Base
  belongs_to :content
  ...
end

class Flag < ActiveRecord::Base
  belongs_to :content
  ...
end

class Publishing < ActiveRecord::Base
  belongs_to :content
  ...
end

class Album < ActiveRecord::Base
  belongs_to :content
  ...
end
...

这些中的每一个都与一个内容相关联,因此外键可以存在于特征的模型中。所有与该功能相关的行为也可以单独存在于该功能的模型上,让 OO 纯粹主义者更快乐。

为了实现通常需要模型挂钩的行为(例如 before_create),我认为观察者模式更有用。 (发送 'content_created' 事件后会创建一个 slug,等等)

这看起来会无休止地清理一切。我现在可以通过单个查询搜索所有内容,数据库中没有重复的字段名称,也不需要将代码包含到内容模型中。

在我愉快地在我的下一个项目中释放它之前,有没有人尝试过这种方法?行得通吗?或者将事情拆分成这么多最终会产生一堆 SQL 查询、连接和混乱的代码?你能推荐一个更好的选择吗?

问题基本上只是混合模式的一个薄包装。对于围绕可重用​​特征编写软件片段来说,这是一种非常有用的模式。

单一Table继承

跨多个模型具有相同列的问题通常通过单一 Table 继承来解决。然而,STI 仅在模型非常相似时才真正适用。

那么让我们考虑一下您的 CMS 示例。我们有几种不同类型的内容:

Page, NewsArticle, BlogPost, Gallery

具有几乎相同的数据库字段:

id
title
content
timestamps
published_at
published_by
# ...

所以我们决定摆脱重复并使用通用的 table。将其称为 contents 很诱人,但这非常模棱两可 - 内容的内容 ...

所以让我们复制 Drupal 并调用我们的通用类型 Node

class Node < ActiveRecord::Base
  include Concerns::Publishable
end

但我们希望每种类型的内容都有不同的逻辑。所以我们为每个类型创建子类:

class Node < ActiveRecord::Base
  self.inheritance_column = :type 
  include Concerns::Publishable
end

class NewsArticle < Node
  has_and_belongs_to_many :agencies
end

class Gallery < Node
  has_and_belongs_to_many :photos
end

# ...

这很有效,直到 STI 模型开始彼此偏离太多。那么数据库模式中的一些重复问题可能比试图将所有内容硬塞进同一个 table 所导致的大量复杂问题要小得多。大多数基于关系数据库构建的 CMS 系统都在努力解决这个问题。一种解决方案是使用无模式的非关系数据库。

构成问题

没有任何内容表明需要您存储模型 table。让我们看看您列出的几个问题:

Flaggable
Sluggable
Commentable

其中每一个都会使用 table flagsslugscomments。关键是使与他们标记的对象的关系,slug 或评论多态。

comment:
  commented_type: string
  commented_id: int
slugs:
  slugged_type: string
  slugged_id: int
flags:
  flagged_type: string
  flagged_id: int
# ...

class Comment
  belongs_to: :commented, polymorphic: true
end

module Concerns::Commentable
  # ...
  has_many: :comments
end

我建议您查看一些解决此类常见任务的库,例如 FriendlyId, ActsAsTaggedOn 等,了解它们的结构。

结论

并行继承的思想从根本上没有错。你应该放弃它只是为了安抚某种极端的 OO 纯度理想的想法是荒谬的。

Traits 是面向对象的一部分,就像任何其他组合技术一样。然而,许多博客文章会让您相信,担忧并不是万灵药。