如何简化Django的迁移和部署

214 阅读5分钟

当从Django模型中删除字段,或添加不可归零的字段时,很难避免在某些服务器上运行的代码和使用的数据库之间出现不匹配。

通过使用django-add-default-value和django-deprecat-fields来简化迁移和部署过程,你将消除一个常见的Django部署头痛问题。这已经是一个挑战了。信不信由你,Django的470号票从2005年起就一直开着!

破损的列

假设你从Django模型中删除了一个或多个字段,也许是因为你正在清理旧的代码,它们不再需要了。不管是什么原因,Django都会创建迁移,将这些列从其数据库表中删除。只要你在任何服务器上运行这些迁移,这些列就会从数据库中消失。这可能是个问题,因为任何运行旧代码的服务器如果仍然引用这些列,就会出现问题。

变通方法

有一个有趣的方法可以让这些列不被破坏。

使用django-deprecat-fields可以在所有服务器仍在运行旧代码时安全地迁移数据库。

你不用从你的模型代码中删除字段,而是像这样用deprecate_field()包住它的定义。

name = deprecate_field(models.CharField(max_length=50))

然后,makemigrations.deprecate_field会将字段定义修改为nullable,Django会创建一个迁移,使该列在数据库中变成nullable。

这样做之后,该列仍然存在,所以即使迁移运行之后,旧的代码仍然可以工作。但它现在是可归零的,所以如果新的代码不在字段上设置值,那也可以工作。

然而,在不使用Django迁移命令时,deprecate_field的行为是不同的:它将字段变成一个假字段,每当它被写入或读出时都会发出警告。

特别是在运行测试或为用户请求服务时,该字段将不再访问数据库。由于这些警告,如果你的测试有良好的覆盖率,这应该有助于发现任何对被删除字段的残留引用,所以它们可以在新的代码中被修复。

当需要部署的时候,尽可能早地应用迁移,这样你就不会在迁移运行后才有服务器运行新的代码,这通常是我们想要的。

考虑的问题

现在,让我们考虑一下可能出现的情况。

  • 如果迁移还没有被应用,服务器正在运行旧的代码,这就是起始状态,估计一切都在工作。
  • 我们故意避开这种状态,即迁移尚未应用,而新代码正在运行(通过确保我们尽可能早地运行迁移)。
  • 如果迁移已被应用,而服务器仍在运行旧的代码,那么该列仍在数据库中,所以旧的代码仍能正常工作。
  • 如果迁移已经应用,而服务器正在运行新的代码,那么该列仍然在数据库中,但新的代码不会读或写它。由于该列已经被修改为可归零(如果它还没有被修改的话),那么就可以在不指定废弃字段的值的情况下更新记录。

当你确信所有的服务器都被更新了,并且你不需要回滚这些变化时,你就可以从你的模型中删除带有废弃字段的一行,然后运行makemigrations。

其结果是,Django会生成一个迁移,将该列从数据库中删除,而在你下次部署时,这个未使用的列就会消失。

向Django模型添加字段也会有类似的陷阱

如果新的字段不能为空,并且在数据库中没有默认值,那么一旦迁移运行,表中就会有一个新的列,但任何旧的代码都不知道要为它提供值,数据库更新就会失败。

设置默认值

使用django-add-default-value可以帮助解决这个问题,因为它可以很容易地为数据库中的新列设置一个默认值。

请记住,Django并没有在数据库中的字段上设置默认值,尤其是当你在字段定义中指定了默认值时。

截至Django 3.1.5,仍然是这样,在你的Django字段定义中设置默认值只导致Django在运行时创建或更新记录时使用该值,但不会使其成为数据库中该列的默认值。

也许有一天Django会获得这种能力。不过现在,使用django-add-default-value可以让你在迁移过程中轻松做到这一点。

要使用django-add-default-value,首先在你的模型中添加你的新字段并运行makemigrations。然后编辑新的迁移。在添加新字段的迁移步骤之后,添加一个由django-add-default-value提供的新迁移步骤,在数据库中设置默认值。

+ from django_add_default_value import AddDefaultValue
+
 operations = [
     migrations.AddField(
         field=models.CharField(default='my_default', max_length=255),
         model_name='my_model'  ,
         name='my_field',
     ),
+    AddDefaultValue(
+        model_name='my_model',
+        name='my_field',
+        value='my_default'
+    )
 ]

现在,一旦迁移运行以创建新列,就会在上面设置一个默认值。

让我们再来看看可能出现的情况。

  • 如果迁移没有被应用,服务器正在运行旧的代码,这就是起始状态,估计一切都在工作。
  • 我们刻意回避了未应用迁移而运行新代码的状态。
  • 如果迁移已经被应用,而服务器仍在运行旧的代码,那么服务器在查询数据库的时候就不会询问新列的值,这很好。如果服务器试图创建或更新记录,它不会为新的字段指定一个值,但是数据库在这种情况下有一个默认值可以使用,它就会工作。
  • 如果迁移已经被应用,并且服务器正在运行新的代码,我们就处于完成状态,新的代码使用新的字段。

与使用django-deprecat-fields时不同,在你部署后不需要清理。

就这样吧!如果你有其他的想法和/或解决方法,我们很乐意在评论中听到它们。