多态关联
简介:始终使用单独的表,而不是多态关联。
Rails使得定义所谓的“多态关联”成为可能。这通常通过向表中添加两列来实现:一个目标类型列和一个目标id。例如,在编写本文时,我们有这样一个设置成员
包括以下栏目:
source_type
:定义要使用的模型的字符串,可以是项目
或名称空间
。source_id
:要检索的行IDsource_type
。例如,当source_type
是项目
然后source_id
将包含一个项目ID。
虽然这样的设置看起来很有用,但它也有许多缺点;足以让你不惜一切代价避免这种情况。
空间浪费
由于这种设置依赖于字符串值来确定要使用的模型,因此最终会浪费大量空间。例如,对于项目
和名称空间
最大大小为9字节,使用PostgreSQL时,每个字符串加1个额外的字节。虽然这可能只有每行10字节,但如果使用这种设置的表和行足够多,我们最终可能会浪费相当多的磁盘空间和内存(对于任何索引)。
索引
由于我们的关联被分解为两个列,这可能导致需要有效地执行查询的复合索引。虽然复合索引完全没有错,但它们的设置可能很棘手,因为这些索引中的列的顺序对于确保最佳性能很重要。
一致性
多态关联的一个真正的大问题是无法使用外键在数据库级别强制数据一致性。为了在数据库级别强制一致性,必须编写自己的外键逻辑来支持多态关联。
在数据库级别强制一致性对于维护健康的环境至关重要,因此这也是避免多态关联的另一个原因。
查询开销
当使用多态关联时,您总是需要使用两个列进行过滤。例如,你可能会写这样一个查询:
选择*从成员在哪里source_type=“项目”和source_id=13083;
在这里,如果两个列都被索引,PostgreSQL可以相当有效地执行查询,但是随着查询变得更复杂,它可能无法有效地使用这些索引。
混合责任
与函数和类类似,表应该只有一个职责:用一组预定义的列存储数据。当使用多态关联时,您将在同一个表中存储不同类型的数据(可能具有不同的列集)。
解决方案
幸运的是,有一个非常简单的解决方案可以解决这些问题:只需为存储在同一表中的每种类型使用单独的表。使用单独的表允许您使用数据库可能提供的一切,以确保一致性和有效地查询数据,而不需要任何额外的应用程序逻辑。
假设有a成员
表中存储项目和组的已批准成员和挂起成员,挂起状态由列确定requested_at
是否确定。在模式方面,这样的设置可能导致仅为某些行设置各种列,从而浪费空间。也有可能只为某些行设置某些索引,再次浪费空间。最后,查询这样一个表需要的查询少于理想查询。例如:
选择*从成员在哪里requested_at是零和source_type=“GroupMember”和source_id=4
相反,这样一个表应该被分解成单独的表。例如,在这种情况下,您可能最终有4个表:
- project_members
- group_members
- pending_project_members
- pending_group_members
这使得查询数据变得微不足道。例如,要获取一个组的成员,你可以运行:
选择*从group_members在哪里group_id=4
要依次获得组中所有待处理的成员,您可以运行:
选择*从pending_group_members在哪里group_id=4
如果您想同时获得这两个查询,则可以使用UNION,但是您需要明确要选择哪些列,否则结果集将使用第一个查询的列。例如:
选择id,“集团”作为target_type,group_id作为target_id从group_members联盟所有选择id,“项目”作为target_type,project_id作为target_id从project_members
上面的示例可能有点傻,但它表明没有什么可以阻止您将数据合并在一起并在同一页面上显示它们。显式选择列还可以加快查询速度,因为数据库需要做更少的工作来获取数据(与选择所有列相比,甚至是不使用的列)。
我们的模式也变得更简单。我们不再需要存储和索引source_type
列时,我们可以很容易地定义外键,并且不需要使用为空
条件。
总而言之:使用单独的表允许我们有效地使用外键,只在必要的地方创建索引,节省空间,更有效地查询数据,并且更容易扩展这些表(例如,通过将它们存储在单独的磁盘上)。这样做的一个很好的副作用是,代码也可以变得更容易,因为您不会最终使用单个模型来处理不同类型的数据。