在Django中运行Python代码作为迁移操作
#python #django #database #backend

此博客文章最初发表于: https://jmplourde.com/til-django-run-python-migration-operation/

当我开始学习Django时,我与makemigrationsmigrate命令之间的区别感到困惑。一段时间后,我明白了,但是我从来没有真正需要与复杂的迁移或写Custome一起工作。我最近不得不编写自定义迁移以将ForeignKey字段更改为OneToOneField,然后使用了RunSQL特殊操作,该操作允许运行RAW SQL查询。它使我能够更好地了解迁移,并使我有信心推动自己的生产变化。

现在,我想将不必要的多一对多模型重构为另一个模型中的一对一字段。模型看起来像以下代码:

from accounts import models as account_models


# the model I want to get rid of
class HomeAddress(BaseModel):
    home = models.ForeignKey(Home, on_delete=models.CASCADE)
    address = models.ForeignKey(
        account_models.Address,
        on_delete=models.CASCADE,
    )
    class Meta:
        verbose_name_plural = "home addresses"


class Home(BaseModel):
    name = models.CharField(max_length=50, default="Home")
    address = models.OneToOneField(  # the field I want to replace the model with
        account_models.Address,
        on_delete=models.PROTECT,
    )

挑战是将每个HomeAddress行中引用的Address复制到引用的Home。我不确定如何解决此问题并害怕RAW SQL查询,而不想弄乱生产数据。我向Chatgpt询问了一些灵感,并建议使用RunPython特殊操作。您首先声明一个有两个参数的函数;第一个是包含与操作相匹配的历史模型的应用程序的名称,第二个是SchemaEditor的实例,将操作转换为SQL。功能代码描述了将要应用的更改。

声明第二个功能,即接受相同的两个参数,其代码应撤销第一个函数所做的事情。这使得迁移可逆,否则更改是永久的。这两个函数将传递给operations类列表中的RunPython函数。对于不支持Data Definition Language (DDL)交易(CREATEALTER等)的数据库后端,RunPython将在交易中运行其内容。

对于确实支持DDL(如PostgreSQL)的数据库,除了由迁移生成的数据库外,没有其他交易。具有模式更改和RunPython操作的迁移将引起一个例外,表明该更改无法应用,因为它具有未决的触发事件。

我最终要做的是生成一个迁移以添加Home.address字段,然后生成一个空迁移并编写以下代码:

from django.db import migrations

from accounts import models as account_models


def replace_home_with_address(apps, schema_editor):
    home_address_model = apps.get_model('nodz', 'HomeAddress')

    for home_address in home_address_model.objects.all():
        address = home_address.address
        address = account_models.Address.objects.create(
        address_line_1=f"123 Fake Street",
        address_line_2="Building 1",
        city="Test City",
        zip_code="12345",
        country_id=home_address.address.country.id,
        subdivision_id=home_address.address.subdivision.id,
        )
        home_address.home.address_id = address.id
        home_address.home.save()


class Migration(migrations.Migration):

    dependencies = [
        ('nodz', '0050_auto_20230821_1308'),
    ]

    operations = [
        migrations.RunPython(replace_home_with_address),
    ]

所以在这里,我绕过HomeAddress行,对于每个行,我将HomeAddress.address引用放入HomeAddress.home.address中并保存。请注意,出于可读性目的,我没有包含查询预取代码。

如果您有任何评论,建议或想法,请在评论中与我分享。