在Rails中改变一个多态的类型
在这篇文章中,我将分享我的队友和我如何重新定义我们在Shopify代码库中存储多态关联的方式。我是新成立的支付灵活性团队的一员。我们的工作是让商家在Shopify上更好地管理他们的付款和应收款项。
Shopify的代码是按组件组织的。作为一个新的团队,我们决定接管一些现有的代码,并将其转移到我们负责的组件(支付灵活性)下。这导致了将类(包括模型)从一个模块移到另一个模块,意味着它们的命名空间必须改变。在思考如何将某些类移到不同的模块下时,我们意识到我们可能会从改变Rails将多态关联持久化到数据库的方式中受益。我们的团队还没有完全就模块和类的命名达成一致。我们希望在项目未来的构建阶段,能够方便地改变名称。
我们决定停止将类名作为某些记录的多态类型来存储。默认情况下,Rails将类名存储为多态类型。我们决定改用一个任意的字符串。这篇文章是对我们如何解决这一难题的一步一步的表述。我之所以说是展示,是因为本文所使用的类和数据并不是来自Shopify代码库的。它们是最初情况和我们应用的解决方案的一个实际例子。
我将从简短地提醒大家什么是多态性开始,然后转向对问题的描述,最后对我们选择的解决方案进行详细说明。
什么是多态性?
多态性是指某一事物具有多种形式(来自希腊语 "polys",表示许多,"morphē"表示形式)。
Rails中的多态关系是指Active Record关联的一种类型。这个概念被用来将一个模型附加到另一个模型上,这个模型可以是不同的类型,只需要定义一个关联。
在这篇文章中,我将以一个Vehicle ,has_one :key and ,Key belongs_to :vehicle 为例。
一个Vehicle 可以是一个Car 或一个Boat 。
在这里你可以看到,Vehicle 有很多形式。Key 和Vehicle 之间的关系是多态的。
存储在子对象上的外键(在我们的例子中是Key 记录)指向一个单一的对象(Vehicle ),它可以有不同的形式(Car 或Boat )。父对象的形式被存储在子对象的polymorphic_type 列下。polymorphic_type 的值等于父对象的类名,在我们的例子中是"Car" 或"Boat" 。
下面的代码块显示了多态关联是如何在Rails中存储的。
问题所在
正如我最初所说,我们的vehicle 类必须移到另一个模块下,模块的改变导致了不同的命名空间。在这个例子中,我假设我想改变我们的代码组织方式,把Car 放在Garage 模块下。
我继续前进,将Car 和Boat 模型移到新的模块Garage 下。
我现在遇到了以下情况:
vehicle_type 列现在包含"Garage::Car" ,这意味着我们将有vehicle_type: "Car" 和vehicle_type: "Garage::Car" 都存储在我们的数据库中。
有了这两个不同的vehicle_type 值,意味着在调用a_vehicle.key 时,Key 记录中的vehicle_type: "Car" 将不会被返回。活动记录关联必须知道vehicle_type 的所有可能值,以便找到相关的记录。
这两个vehicle_type 的值都应该指向更新的模型Garage::Car ,这样我们的多态ActiveRecord关联才能继续工作。关联在两个方向上都被打破了。在有vehicle_type: "Car" 的Key 记录上调用#vehicle ,不会返回相关的记录。
构想
一旦我们意识到改变命名空间会带来复杂性和一系列的任务(见下一段),我的一个队友对我说,"让我们完全停止在数据库中存储类名。通过把类名变成一个任意的字符串,我们可以减少代码库和数据库之间的耦合。这意味着我们可以更容易地改变类名和命名空间,如果我们将来需要的话"。对于我们的例子,与其存储"Garage::Car" 或"Garage::Boat" ,为什么我们不直接存储"car" 或"boat" 呢?
为了推进模块和类名的改变,而不修改Active Record存储多态关联的方式,我们必须在设置ActiveRecord关联时增加从几个多态类型中读取的能力。我们还必须更新现有的记录,使其指向新的命名空间。如果我们回到我们的例子,带有vehicle_type: "Garage::Car" 的记录应该指向新的Garage::Car 模型,直到我们能够用更新的模型类名称对该列进行回填。
在实践中:从存储类名到一个任意的字符串
Rails有一种方法来覆盖polymorphic_type 值的写入。它是通过重新定义polymorhic_name 方法来实现的。下面的代码取自Rails gem的源代码。
让我们为我们的Garage::Car 示例重新定义上面的源代码。
当创建一个Key 记录时,我们现在有以下情况。
现在我们有了"Car" 这个类名和"car" 这个任意的字符串,存储为vehicle_type 。为vehicle_type 有两个可能的值带来了另一个问题。在多态关联中,目标(关联记录)是使用.polymorphic_name 中返回的单一值来查找的,而这正是限制所在。该关联只能查找一个vehicle_type 的值。vehicle_type 被存储为创建记录时由polymorphic_name 返回的值。
这个限制的一个例子:
仔细看一下SQL表达式,你会发现我们只在寻找带有vehicle_type = "car" (任意字符串)的键。关联不会找到我们开始修改代码之前创建的车辆的Key (键值为vehicle_type = "Car" )。vehicle_type 我们必须重新定义我们的关联范围,这样它就可以寻找带有"Car" 或"car" 的键。
我们的关联现在变成了下面的SQL表达式:
联想现在正在寻找带有"car" 或"Car" 的键,作为vehicle_type 。
现在我们可以读取类名和新的任意字符串作为我们的关联的vehicle_type ,我们可以继续清理我们的数据库,只将任意字符串存储为vehicle_type 。在Shopify,我们使用MaintenanceTasks。你可以运行一个迁移或像下面这样的脚本来更新你的记录。
一旦清理完成,我们只有任意字符串存储为vehicle_type 。我们可以继续前进,删除Garage::Car 和Garage::Boat 关联上的.unscope 。
但是,等等,这一切是为了什么?
这个补丁的主要好处是,我们减少了代码库和数据库之间的耦合。
不把类名作为多态类型来存储,意味着你可以移动你的类,重新命名你的模块和类,而不必去碰你现有的数据库记录。你所要做的就是更新在三个CLASS_MAPPING 哈希中作为键和值的类名。存储在数据库中的值将保持不变,除非你改变这些类和类名所解析的任意字符串。
我们的解决方案增加了复杂性。对于大多数用例来说,这可能不值得。对我们来说,这是一个很好的交易,因为我们知道我们的模块和类的命名在不久的将来可能会改变。
我解释的解决方案并不是我们最初采用的方案。我们最初走的是一条更复杂的路线。这篇文章是我们希望在开始研究改变多态关联的存储方式时能找到的解决方案。经过一番研究和实验,我得出了这个简化版本,并认为它值得分享。