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

时间:2015-06-20 17:15:17

标签: ruby-on-rails database database-design architecture separation-of-concerns

我阅读了很多博客,其中一个经常出现的主题是关注点(至少是Rails定义它们的方式)对软件有害。总而言之,我同意 - 简单地将行为纳入模型违反了单一责任原则。你最终会得到一个太过分的神级。

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

所以让我们来看一个示例应用程序,松散地基于我必须维护的应用程序。它本身就是一个CMS,因为许多Rails应用程序往往是。

目前,每个模型都有很多问题。我们在这里使用一些:

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

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

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

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

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

如果我们添加一种新内容会发生什么?

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

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

然后我添加了更多内容类型:事件和个人资料,比如说。

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

  • 我们有多个具有完全相同字段的数据库表 (flagged_by, published_on等。)
  • 我们无法使用单个SQL查询一次检索多个内容类型。
  • 每个模型都支持包含关注点的重复字段名称,为每个类赋予多重职责。

那么什么是更好的方式?

我已经看到装饰器被提升为一种在需要时添加功能的方法。这可以帮助解决包含代码的问题,但数据库结构不一定得到改进。它看起来也不必要,并且需要在代码中添加额外的循环来装饰模型数组。

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

创建一个通用的“内容”模型(和表):

class Content < ActiveRecord::Base
end

关联表可能非常小。它可能应该有某种“类型”字段,也许绝对所有内容都有一些共同点 - 比如URL的类型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查询,连接和纠结的代码?你能建议一个更好的选择吗?

1 个答案:

答案 0 :(得分:2)

担忧基本上只是mixin模式的一个薄包装。这是围绕可重用​​特征组合软件的非常有用的模式。

单表继承

通过单表继承解决了跨多个模型具有相同列的问题。然而,当模型非常相似时,STI才真正适合。

因此,请考虑您的CMS示例。我们有几种不同类型的内容:

Page, NewsArticle, BlogPost, Gallery

其中包含几乎完全相同的数据库字段:

id
title
content
timestamps
published_at
published_by
# ...

因此我们决定摆脱重复并使用公共表。称之为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模型开始相互分离太多。然后,数据库模式中的一些重复可能是一个小问题,而不是因为试图将所有东西都塞进同一个表中而导致的大量复杂问题。在关系数据库上构建的大多数CMS系统都很难解决这个问题。一种解决方案是使用无模式非关系数据库。

撰写问题

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

Flaggable
Sluggable
Commentable

其中每个都会使用表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

我建议您查看一些解决这些常见任务的库,例如FriendlyIdActsAsTaggedOn等,以了解它们的结构。

结论

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

特征是面向对象的一部分,只是任何其他组合技术。然而,令人担忧的不是许多博客文章让你相信的神奇修复。