迁移Django模型字段名称更改而不丢失数据

89

我有一个Django项目,其中有一个包含数据的数据库表。我想更改该字段的名称,而不会丢失该列中的任何数据。我的原始计划是通过以不会实际更改DB表名称的方式更改模型字段名称(使用db_column列参数):

原始模型:

class Foo(models.Model):
  orig_name = models.CharField(max_length=50)

新模型:

class Foo(models.Model):
  name = models.CharField(max_length=50, db_column='orig_name')

但是,运行South的 schemamigration --auto 会生成一个迁移脚本,其中删除了原始列 orig_name 并添加了一个新列 name。这将导致该列中的数据被删除,这是一个不需要的副作用。(我也很困惑为什么South要更改数据库中的列名,因为我理解的 db_column 是允许在不更改数据库表列名称的情况下更改模型字段名称)

如果不能在不更改db字段的情况下更改模型字段,我想我可以通过以下方式进行更直接的名称更改:

原始模型:

class Foo(models.Model):
  orig_name = models.CharField(max_length=50)
新模型:
class Foo(models.Model):
  name = models.CharField(max_length=50)

无论我最终采用哪种策略(我更喜欢第一种,但会接受第二种),我的主要关注点是确保不会丢失该列中已有的数据。

这是否需要一个多步骤的过程?(例如1.添加一列,2.将数据从旧列迁移到新列,3.删除原始列) 还是我可以通过像 db.alter_column 这样的方式修改迁移脚本?

在更改该列的名称时,保留该列中的数据的最佳方法是什么?

11个回答

74

改变字段名称而保留数据库字段

对于 Django 1.8+(使用 Django 原生迁移,而非 South),添加一个答案。

创建一个迁移,先添加一个 db_column 属性,然后再重命名字段。Django 理解第一个操作是无操作(因为它将 db_column 更改为相同的值),第二个操作也是无操作(因为它不会进行模式更改)。事实上,我检查了日志以确保没有进行模式更改...

operations = [
    migrations.AlterField(
        model_name='mymodel',
        name='oldname',
        field=models.BooleanField(default=False, db_column=b'oldname'),
    ),
    migrations.RenameField(
        model_name='mymodel',
        old_name='oldname',
        new_name='newname',
    ),
]

3
这取决于您是否想要在数据库中重命名该列。如果没有使用AlterField,您会进行模式更改。 - Aaron McMillin
不适用于Django 2+。但是相同的逻辑可以使用related_name来实现 => 如果保持不变,Django将检测到该字段已被重命名。 - lapin
4
注意!这些操作在某些情况下会触发更改,例如对于外键(DROP/SET CONSTRAINT)。我是通过切身经历学到的(在生产环境中)。我创建了一个 Django 票据,并提出请求以解决这些情况。 https://code.djangoproject.com/ticket/31825 https://code.djangoproject.com/ticket/31826话虽如此,您可以使用 manage.py sqlmigrate <app_name> <migration> 来检查它将运行的 SQL 命令。这取决于数据库,因此请使用与生产环境中相同类型的数据库来运行它。 - iurisilvio
@iurisilvio 如何避免添加/删除约束?有没有什么方法可以避免它? - yogi
@iurisilvio 这是类似这样的代码。migrations.AlterField( model_name='invoice', name='payment', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='billing.Payment', db_column='payment'), ), migrations.RenameField( model_name='invoice', old_name='payment', new_name='_deprecated_payment', ), - yogi
显示剩余3条评论

64

Django 2.0.9(及以后版本)可以自动检测字段是否被重命名,并提供重命名选项而非删除并创建新的字段。

Django 2.2 同样适用)enter image description here

初步回答

如果对某人有帮助,可以发布。 对于 Django 2.0 +,只需在模型中重命名字段即可。

class Foo(models.Model):
    orig_name = models.CharField(max_length=50)

class Foo(models.Model):
    name = models.CharField(max_length=50)

现在运行python manage.py makemigrations 命令,它将生成包含删除旧字段和添加新字段操作的迁移文件。

接下来将其更改为以下内容。

operations = [
    migrations.RenameField(
        model_name='foo',
        old_name='orig_name',
        new_name='name')
]

现在运行python manage.py migrate,它将在不丢失数据的情况下重命名数据库中的列。


好的,它确实可以工作。不确定你的使用情况,能否提供一些细节?事实上,在Django 2.0.9中,如果我重命名字段,它提供了选项。我已经更新了答案。 - Aarif
2
我感谢你的回答@Aarif,对我很有帮助。如果你只是重命名一个或两个字段,使用这种方法应该没问题。但是,如果你一次要重命名多个字段(这本来就不推荐),可能会遇到问题。 - Jihoon Baek
请注意保罗·塞巴斯蒂安的答案中提到的引用旧列名的表单会导致迁移错误(Django 2.2.5)。 - JL Peyret
6
补充一点,如果你想这样做,请确保同时不更改字段中的任何其他内容,否则Django将不会将其检测为名称更改,而是视为删除字段和添加新字段。我尝试同时删除“blank=True, null=True”并设置默认值,但它没有起作用。一旦我恢复了原来的设置,并仅更改名称,它就可以工作了! - Luca Bezerra
@LucaBezerra 同意,这也抓住了我。即使同时更改非关系属性(如verbose_name),也会阻止Django建议RenameField操作。在单独的迁移中进行任何其他更改,然后将字段重命名为最终(或第一)步骤。 - BigglesZX
显示剩余3条评论

18

修复起来相当容易。但您将不得不自己修改迁移文件。

不要删除并添加该列,而要使用db.rename_column。您只需修改由schemamigration --auto创建的迁移文件即可。


太好了!我以为会有一个简单的解决方案。所以,如果我向前添加以下内容: db.rename_column('myapp.foo', 'orig_name', 'name') 并向后添加以下内容: db.rename_column('myapp.foo', 'name', 'orig_name') 这些更改应该添加到包括对模型定义进行更改的迁移脚本中吗?还是在创建迁移脚本之前不应手动更改模型定义?(如果我的问题不清楚,请告诉我,我会用不同的方式解释) - sfletche
没事了,那是个愚蠢的问题。我会先更改模型定义,然后创建一个空的迁移脚本,并修改前向和后向方法以包含我上面提到的重命名列数据。谢谢! - sfletche
1
例子:https://dev59.com/PHA75IYBdhLWcg3wi5sr - andilabs

10

实际上,使用Django 1.10只需在模型中重命名字段,然后运行makemigrations,即可立即识别操作(即一个字段消失,另一个字段代替它):

$ ./manage.py makemigrations
Did you rename articlerequest.update_at to articlerequest.updated_at (a DateTimeField)? [y/N] y
Migrations for 'article_requests':
  article_requests/migrations/0003_auto_20160906_1623.py:
    - Rename field update_at on articlerequest to updated_at

2
只是想强调一下,Django实际上会询问您是否要重命名(我想您可以回答“否”并取消正在运行的makemigrations)。 - Ben
确认过了,也在Django 2.2.5上工作。然而...迁移一开始就失败了,不是因为模型或数据库直接相关的问题,而是一个ModelForm使用了旧的列名。所以在运行迁移之前,你需要清理表单和模型本身。我宁愿表单在使用中失败,而不是DDL迁移错误,但事实就是这样。在Postgres中进行更改很容易,但是Django迁移却完全卡住了,所以我恢复了DDL重命名,从git还原并正确地完成了操作。 - JL Peyret
@JLPeyret 我认为你应该阅读问题标题,我已经在 Django 2.2.9 上进行了测试,如果你硬编码字段名称,它可以正常工作,这是作为开发人员你的责任,要做出适当的更改,就 Django 而言,它正在按预期工作。 - Aarif
我的评论绝不是对你正确回答的批评。 - JL Peyret
1
注意事项:为了使迁移检测并询问名称更改,您不能更改冗长名称(字段定义中的可选第一个位置参数)。如果同时更改字段名称和冗长名称,则Django迁移会删除旧名称字段并添加新名称字段。解决方案是首先保持冗长名称不变,运行make migrations,然后再更改冗长名称。 - Rick Graves
显示剩余2条评论

5
我遇到了这种情况。我想在模型中更改字段名称,但保持列名不变。
我的做法是执行 schemamigration --empty [app] [some good name for the migration]。问题在于,就South而言,在模型中更改字段名称是需要处理的一项更改。因此,必须创建一个迁移。然而,我们知道数据库方面没有任何操作需要执行。因此,空迁移避免在数据库上执行不必要的操作,同时满足South需要处理其认为是更改的内容。 请注意,如果您使用loaddata或使用Django的测试fixture功能(它在后台使用loaddata),则必须更新fixture以使用新字段名称,因为fixture基于模型字段名称而不是数据库字段名称。
对于数据库中列名确实更改的情况,我从不建议使用db.rename_column进行列迁移。我使用sjh在此答案中描述的方法:

我已将新列添加为一个schemamigration,然后创建了一个datamigration来将值移动到新字段中,然后创建了第二个schemamigration来删除旧列

如我在该问题的评论中所述,db.rename_column的问题在于它不会将约束与列一起重命名。无论问题只是表面上的还是未来的迁移失败可能会因为找不到约束而导致,这对我来说都是未知的。

4

可以在不进行手动迁移文件编辑的情况下重命名字段:

▶︎ 从类似以下内容开始:

class Foo(models.Model):
  old_name = models.CharField(max_length=50)

▶︎ 在原始字段中添加db_column=OLD_FIELD_NAME

class Foo(models.Model):
  old_name = models.CharField(max_length=50, db_column='old_name')

▶︎ 运行: python3 manage.py makemigrations

▶︎ 将字段从 OLD_FIELD_NAME 重命名为 NEW_FIELD_NAME

class Foo(models.Model):
  new_name = models.CharField(max_length=50, db_column='old_name')

▶︎ 运行命令:python3 manage.py makemigrations

您将会看到以下提示:

您是否已将 MODEL.OLD_FIELD_NAME 重命名为 MODEL.NEW_FIELD_NAME(一个 ForeignKey)?[y/N] y

这将生成两个迁移文件而不是一个,尽管这两个迁移都是自动生成的。

此过程适用于 Django 1.7 及以上版本。


“在原始字段中添加db_column=OLD_FIELD_NAME。” -> 呃?在哪里?我不理解这个答案。 - NaturalBornCamper
@NaturalBornCamper,我添加了一些示例代码,应该可以使步骤更容易理解。 - David Foster
这只是重命名模型字段,数据库列名保持不变。进一步地,如何在没有任何停机时间的情况下重命名数据库列呢? - Ehtesham Siddiqui
关于“如何在没有任何停机时间的情况下重命名数据库列”,这比较棘手,最好作为一个新问题提出。 - David Foster

3
这是针对Django 4.0的。 让我们通过一个例子来完成这个操作。
我的原始字段名称是anticipated_end_date,我需要将其命名为tentative_end_date。按照以下步骤完成此操作:
1. 在模型内将anticipated_end_date更改为tentative_end_date 2. 运行python manage.py makemigrations。理想情况下,它会显示以下消息
`您的模型名称.anticipated_end_date是否重命名为your_model_name.tentative_end_date(DateField)?[y / N]`
如果它显示这条消息,则只需按y并且您可以迁移了,因为它将生成正确的迁移。但是,如果makemigrations命令没有询问关于重命名模型字段,则进入生成的迁移并以以下方式更改operations内容:
    operations = [
        migrations.RenameField(
            model_name='your_model_name',
            old_name='anticipated_end_date',
            new_name='tentative_end_date',
        ),
    ]

现在您可以运行 python manage.py migrate 命令。这样,您的模型字段/数据库列将被重命名,而数据不会丢失。

2

我在Django 1.7.7中遇到了这种情况。最终我采取了以下措施,这对我很有效。

./manage.py makemigrations <app_name> --empty

新增了一个简单的migrations.RenameField子类,它不会触及数据库:

class RenameFieldKeepDatabaseColumn(migrations.RenameField):
def database_backwards(self, app_label, schema_editor, from_state, to_state):
    pass

def database_forwards(self, app_label, schema_editor, from_state, to_state):
    pass

2

更新Django 3.1 中,仅更改一个字段非常简单。

在我的情况下:

旧字段名称为:is_admin 新字段名称为:is_superuser

当我通过 python manage.py makemigrations 进行迁移时,它会询问我是否要重命名字段。我只需按 y 键进行重命名。然后我通过 python manage.py migrate 进行迁移。在我的情况下,终端历史记录如下所示: enter image description here

注意:我没有测试过同时更改多个字段的情况。


0

正如其他回答所指出的那样,使用db_column现在已经很容易对字段进行重命名,而不会对数据库造成任何更改。但是生成的迁移实际上将创建一些SQL语句。您可以通过在迁移上调用 ./manage.py sqlmigrate ... 来验证。

为了避免对您的数据库产生任何影响,您需要使用 SeparateDatabaseAndState 来告诉Django它不需要在数据库中执行任何操作。

如果您想了解更多信息,我写了一篇关于此的小文章


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接