Help us learn about your current experience with the documentation. Take the survey.

多态关联

摘要:始终使用单独的表,而不是多态关联。

Rails 允许定义所谓的"多态关联"。这通常通过向表中添加两列来实现:目标类型列和目标 ID。例如,在撰写本文时,我们对 members 表使用了这样的设置,包含以下列:

  • source_type:定义要使用的模型的字符串,可以是 ProjectNamespace
  • source_id:基于 source_type 检索的行的 ID。例如,当 source_typeProject 时,source_id 包含一个项目 ID。

虽然这种设置可能看起来很有用,但它带来了很多缺点;严重到你应该不惜一切代价避免使用它。

空间浪费

因为这种设置依赖于字符串值来确定要使用的模型,它浪费了大量空间。例如,对于 ProjectNamespace,最大大小为 9 字节,使用 PostgreSQL 时每个字符串还要额外占用 1 字节。虽然每行可能只占 10 字节,但考虑到使用这种设置的表和行足够多,我们最终可能会浪费相当多的磁盘空间和内存(对于任何索引)。

索引

因为我们的关联被拆分为两列,这可能导致需要复合索引才能高效执行查询。虽然复合索引完全没有问题,但它们可能难以设置,因为这些索引中列的顺序对于确保最佳性能很重要。

一致性

多态关联的一个真正大问题是无法使用外键在数据库级别强制执行数据一致性。要在数据库级别强制执行一致性,必须编写自己的外键逻辑来支持多态关联。

在数据库级别强制执行一致性对于维护健康环境至关重要,因此这也是避免使用多态关联的另一个原因。

查询开销

使用多态关联时,你总是需要使用两列进行过滤。例如,你可能会编写如下查询:

SELECT *
FROM members
WHERE source_type = 'Project'
AND source_id = 13083;

如果这两列都被索引,PostgreSQL 可以相当高效地执行此查询。但随着查询变得更复杂,它可能无法有效使用这些索引。

混合职责

类似于函数和类,表应该有单一职责:使用一组预定义的列存储数据。使用多态关联时,你是在同一张表中存储不同类型的数据(可能具有不同的列集)。

解决方案

幸运的是,这些问题有一个解决方案:为原本要存储在同一张表中的每种类型使用单独的表。使用单独的表允许你使用数据库可能提供的所有功能来确保一致性和高效查询数据,而不需要任何额外的应用程序逻辑。

考虑一个 members 表,它存储项目和组的已批准和待批准成员。要确定成员是否待批准,我们检查 requested_at 列是否设置了值。从架构角度看,这种配置只为某些行设置一些索引和列,可能会浪费空间。查询此表也需要次优查询。例如:

SELECT *
FROM members
WHERE requested_at IS NULL
AND source_type = 'GroupMember'
AND source_id = 4

相反,这样的表应该被拆分为单独的表。例如,在这种情况下你最终可能会得到 4 个表:

  • project_members
  • group_members
  • pending_project_members
  • pending_group_members

这使得数据查询变得简单。例如,要获取一个组的成员,你可以运行:

SELECT *
FROM group_members
WHERE group_id = 4

要依次获取一个组的所有待批准成员,你可以运行:

SELECT *
FROM pending_group_members
WHERE group_id = 4

如果你想获取两者,可以使用 UNION,但需要明确指定要 SELECT 的列,否则结果集将使用第一个查询的列。例如:

SELECT id, 'Group' AS target_type, group_id AS target_id
FROM group_members

UNION ALL

SELECT id, 'Project' AS target_type, project_id AS target_id
FROM project_members

上面的例子可能有点傻,但它表明没有什么能阻止你将数据合并在一起并在同一页面上呈现。显式选择列也可以加快查询速度,因为数据库需要做更少的工作来获取数据(与选择所有列,包括你未使用的列相比)。

我们的架构也变得更简单。我们不再需要存储和索引 source_type 列,可以轻松定义外键,也不需要使用 IS NULL 条件来过滤行。

总结:使用单独的表允许我们有效地使用外键,只在必要时创建索引,节省空间,更高效地查询数据,并且更容易扩展这些表(例如,通过将它们存储在不同的磁盘上)。这样做的一个很好的副作用是代码也可以变得更简单,因为单个模型不再负责处理不同类型的数据。