【问题标题】:Bulk create auto fields in Django在 Django 中批量创建自动字段
【发布时间】:2021-05-02 12:51:32
【问题描述】:

我有两个模型,我想将所有数据从一个模型迁移到另一个模型。为了模拟问题,假设以下模型:

from django.db import models

class Book:
    name = models.CharField(max_length=100)
    is_archived = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)


class BookArchived:
    name = models.CharField(max_length=100)
    created_at = models.DateTimeField(auto_now_add=True)

迁移的代码是:

book_objs = []
for archived_book in BookArchived.objects.all():
    book_objs.append(Book(name=archived_book.name, is_archived=True, created_at=archived_book.created_at))

Book.objects.bulk_create(book_objs)

这段代码的问题在于,虽然我明确设置了created_at 字段(created_at=archived_book.created_at),但 Django 将所有这些字段都插入了当前时间。

我知道,如果明确设置了 pk 字段 (id=archived_book.id),新对象将保留原始时间戳。但是数据库中已经有重叠的ID,所以这种方法会产生另一个问题。如何保留 BookArchived 实例的原始 created_at 值?

【问题讨论】:

  • @blondelg:就是这样。
  • 我不明白你为什么需要一个额外的模型。您有一个布尔值来确定 Book is_archived 是否为,因此您可以轻松提取已归档的书籍。
  • @WillemVanOnsem 它是旧模型。现在数据存在于两个不同的表中。这就是为什么我想将所有数据从BookArchived迁移到Book,然后删除之前的模型。
  • @ElginCahangirov 您需要先从Book 模型迁移中删除auto_now_add=True,然后在另一个迁移中将其添加回来。 auto_now_add 隐式设置 editable=False 所以你不能自己设置它的值。
  • @AbdulAzizBarkat 对于生产环境来说是不可接受的解决方案。在部署时会有插入到预定表(Book 模型)

标签: python django


【解决方案1】:

我发现这个 gem https://stackoverflow.com/a/59898220/519995 应该可以工作,但我自己没有测试过。


from contextlib import contextmanager

@contextmanager
def suppress_auto_now(model, field_names):
    """
    From https://stackoverflow.com/a/59898220/519995
    idea taken here https://stackoverflow.com/a/35943149/1731460
    """
    fields_state = {}
    for field_name in field_names:
        field = model._meta.get_field(field_name)
        fields_state[field] = {'auto_now': field.auto_now, 'auto_now_add': field.auto_now_add}

    for field in fields_state:
        field.auto_now = False
        field.auto_now_add = False
    try:
        yield
    finally:
        for field, state in fields_state.items():
            field.auto_now = state['auto_now']
            field.auto_now_add = state['auto_now_add']

像这样使用它:

with suppress_autotime(Book, ['created_at']):
        Book.objects.bulk_create(book_objs)

注意: 不要在您的视图/表单或 Django 应用程序的任何地方使用此上下文管理器。此上下文管理器更改字段的内部状态(通过临时将auto_nowauto_now_add 设置为False)。这将导致 Django 在为并发请求(即相同的进程,不同的线程)执行上下文管理器的主体期间不使用timezone.now() 填充这些字段。虽然这可用于独立脚本(例如管理命令、数据迁移),但它们与 Django 应用程序不在同一进程中运行。

【讨论】:

  • 此解决方案在这种情况下按预期工作。但我认为这在视图/表单或项目中的任何地方使用都是危险的,因为上下文管理器正在改变模型字段的内部状态。此更改将对运行 django 应用程序的整个过程产生影响,因此 django 在从上下文管理器进入和退出时间之间保存时不会自动填充字段。你怎么看?
  • 我在 django 视图中测试了这段代码。正如我所料,它导致尝试创建新的Book 实例的并发请求出错,因为suppress_auto_now 上下文管理器改变了created_at 字段的状态。因此,在 django 应用程序中使用此代码是不安全,尽管它符合我的需要。我将在 django 管理命令中运行迁移代码,该命令将在 django 应用程序的单独进程中执行。
  • 请接受我的编辑,将您的答案标记为已接受。
  • 你是对的@ElginCahangirov!如果在网络服务器内部使用这确实很危险,我只是在考虑在服务器进程之外运行的数据迁移脚本。 - 编辑接受,谢谢!
【解决方案2】:

根据DateFielddocumentation 中的注释:

按照目前的实现,将 auto_now 或 auto_now_add 设置为 True 将导致该字段设置为 editable=False 和 blank=True。

由于editable 将设置为False,因此向模型提供任何值都将不起作用。您需要在多次迁移中执行此步骤。

先有没有auto_now_add的模型:

class Book(models.Model):
    name = models.CharField(max_length=100)
    is_archived = models.BooleanField(default=False)
    created_at = models.DateTimeField()

使用python manage.py makemigrations为此生成迁移。接下来我们要做一个data migration [Django docs]。首先运行python manage.py makemigrations --empty <yourappname>,这将创建一个空迁移,您将对其进行编辑以将数据从BookArchived 复制到Book。这看起来像:

from django.db import migrations

def copy_legacy_books(apps, schema_editor):
    # We can't import the Person model directly as it may be a newer
    # version than this migration expects. We use the historical version.
    Book = apps.get_model('yourappname', 'Book')
    BookArchived = apps.get_model('yourappname', 'BookArchived')
    book_objs = []
    for archived_book in BookArchived.objects.all():
        book_objs.append(Book(name=archived_book.name, is_archived=True, created_at=archived_book.created_at))
    
    Book.objects.bulk_create(book_objs)

class Migration(migrations.Migration):

    dependencies = [
        ('yourappname', '0001_initial'),
    ]

    operations = [
        migrations.RunPython(copy_legacy_books),
    ]

现在只需将auto_now_add kwarg 添加到您的日期时间字段并使用python manage.py makemigrations 生成另一个迁移:

class Book:
    name = models.CharField(max_length=100)
    is_archived = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

【讨论】:

  • 感谢您的努力。提出的解决方案克服了我的问题。但请考虑对应用程序的高度并发请求。虽然我的自定义迁移执行 Book.created_at 不会是 auto_now_add 这将导致服务器错误。
  • @ElginCahangirov 这将如何导致错误?在应用所有迁移之前,服务器甚至不应该运行。考虑到 python manage.py migrate 运行所有这 3 个迁移将按顺序应用,之后您可以根据需要或使用固定装置在另一个数据迁移中添加一些书籍实例(根据您的评论“在部署时会有插入预订餐桌”)。在完成所有迁移并加载初始数据之后,理想情况下服务器应该使用python manage.py runserver 运行
  • @ElginCahangirov 请参阅docs.djangoproject.com/en/3.2/howto/initial-data,了解如何为模型提供初始数据。
  • 假设数据迁移需要 2 分钟(当然其他 2 次迁移需要的时间要少得多)。在这 2 分钟内,Bookcreated_at 字段将不会是 auto_now_add=True。因此,对于尝试将 book 插入数据库的请求将导致服务器错误(由于 django 不会填充此列,因此数据库将抱怨 not null 约束违规)。至于你所说的server should be run after all the migrations are done and initial data is loaded,在 ci/cd 环境中,比如使用 kubernetes 部署,情况并非如此。应用程序应该在部署期间处于活动状态。
  • 一般应用程序在数据库迁移结束后更新。所以新表不会引起问题,因为旧应用程序实例还不知道新模型。创建新表后,旧应用程序将正常终止,创建新表并将请求分派到新实例。 Django 的一些行为在 CI/CD 期间会导致问题。例如,请参阅this ticket
猜你喜欢
  • 2022-01-06
  • 1970-01-01
  • 1970-01-01
  • 2017-02-08
  • 2011-08-26
  • 2018-04-27
  • 1970-01-01
  • 2014-02-03
  • 2014-02-21
相关资源
最近更新 更多