为啥 Django 在添加新列时会删除 SQL DEFAULT 约束?

Posted

技术标签:

【中文标题】为啥 Django 在添加新列时会删除 SQL DEFAULT 约束?【英文标题】:Why does Django drop the SQL DEFAULT constraint when adding a new column?为什么 Django 在添加新列时会删除 SQL DEFAULT 约束? 【发布时间】:2020-01-24 09:28:12 【问题描述】:

在最新的 Django (2.2) 中,当我向这样的模型添加新字段时:

new_field= models.BooleanField(default=False)

Django 为 mysql 运行以下命令:

ALTER TABLE `app_mymodel` ADD COLUMN `new_field` bool DEFAULT b'0' NOT NULL;
ALTER TABLE `app_mymodel` ALTER COLUMN `new_field` DROP DEFAULT;
COMMIT;

虽然这在更新所有内容时有效,但这是非常有问题的,因为在运行此迁移后旧版本的应用程序无法再创建模型(他们不知道 new_field)。为什么不保留DEFAULTconstraint?

【问题讨论】:

这纯粹是一个理论问题,还是您也在寻找解决方案? 我正在寻找一种在没有null=True 的情况下安全地添加新字段的方法。用default=False 添加BooleanField 不起作用。这会使旧版本的应用崩溃。 我不明白这最后的评论。您要么想要一个具有两种状态或三种状态的字段:models.BooleanField()models.BooleanField(null=True)。无论哪种方式,除非您指定默认值,否则默认值为 None,就像您在示例中所做的那样。 【参考方案1】:

为什么不只保留DEFAULT 约束?

因为 Django 在应用程序级别而不是数据库级别处理 default 模型字段选项。所以真正的问题是它为什么要设置DEFAULT 约束。

但首先:Django 不使用数据库级别的默认值。 (来自the documentation:“Django 从不设置数据库默认值,总是在 Django ORM 代码中应用它们。”)。从项目一开始就是如此。一直有人对更改它感兴趣(这个主题的 first issue 已有 14 年历史),当然其他框架(我认为是 Rails 和 SQLAlchemy)已经表明这是可能的。

但是,除了向后兼容之外,还有很多理由可以在应用程序级别处理默认值。如:表达任意复杂计算的能力;不必担心跨数据库引擎的细微不兼容性;能够在代码中实例化新实例并立即访问默认值;能够在表单中向用户呈现默认值;等等。

基于关于该主题的两个mostrecent 讨论,我想说将数据库默认值合并到default 的语义中的兴趣不大,但支持添加一个新的db_default 选项。

现在,向现有数据库添加一个新的不可为空的字段是一个非常不同的用例。在这种情况下,您必须为数据库提供默认值才能执行操作。如果可以,makemigrations 将尝试从您的 default 选项推断出正确的值,否则将强制您从命令行指定一个值。所以DEFAULT 修饰符用于这个有限的目的,然后被删除。

正如您所注意到的,Django 中缺少数据库级别的默认设置会使持续部署变得更加困难。但解决方案相当简单:只需在迁移中自己重新添加默认值。迁移系统的一大好处是它可以轻松地在 Django 的 ORM 之外对数据库进行任意、可重复、可测试的更改。所以只需添加一个新的RunSQL 迁移操作:

operations = [
    # Add SQL for both forward and reverse operations
    migrations.RunSQL("ALTER TABLE app_mymodel ALTER COLUMN new_field SET DEFAULT 0;",
                      "ALTER TABLE app_mymodel ALTER COLUMN new_field DROP DEFAULT;")
]

您可以将其放入new migration file 或简单地编辑自动生成的。根据您的数据库及其对事务性 DDL 的支持,操作序列可能是原子的,也可能不是原子的。

【讨论】:

我认为这是正确的答案。我自己不是 RunSQL 迁移的粉丝,但也许我应该重新考虑。【参考方案2】:

我找到了这张 2 年前的票:https://code.djangoproject.com/ticket/28000 是stated in there:

Django 使用数据库默认值来设置表中现有行的值。它不会在数据库中保留默认值,因此删除默认值是正确的行为。在这种情况下可能有一个优化不设置/删除默认值 - 我不确定它是否需要,因为该列不为空。可以为此单独开一张票。

我在这里的另一个问题中也看到了相同的参考:Django Postgresql dropping column defaults at migrate

再搜索一下,我发现了这个 SO 问题:Django implementation of default value in database 导致出现此评论的 _alter_field method from django.db.backends.base.schema 的代码:

# When changing a column NULL constraint to NOT NULL with a given
# default value, we need to perform 4 steps:
#  1. Add a default for new incoming writes
#  2. Update existing NULL rows with new default
#  3. Replace NULL constraint with NOT NULL
#  4. Drop the default again.

虽然最后一个是关于将现有的可为空字段更改为不可为空的,但这似乎是 Django 处理default 案例的方式:/

【讨论】:

感谢您的参考!我不确定我是否理解为什么。而且我仍然不明白如果没有null=True,我怎么能安全地添加新字段,但这是一个单独的问题。 @Petter 也许如果您尝试添加新字段,然后当您即将应用迁移./manage.py migrate 它会要求您提供默认值。 (我没有尝试过,所以我只是建议尝试一下) 创建迁移时应该出现该问题,而不是应用。 @Petter 没错。那么这是否有效或导致相同的行为? 您似乎在询问是否要添加没有默认值且 null=False 的字段?之后,旧版本的软件也会崩溃。

以上是关于为啥 Django 在添加新列时会删除 SQL DEFAULT 约束?的主要内容,如果未能解决你的问题,请参考以下文章

在 VB 2010 的数据表中插入新列时,如何更新 SQL Server 2008 数据库?

在 Mysql 中添加新列时如何轻松维护审计触发器

添加新列时在alembic中设置列顺序

为啥查询中没有提到任何命名列时会出现新列错误?

为啥在尝试访问 HTML 表中的前两列时会出现错误?

为啥添加一列时所有行都得到空值?