gpt4 book ai didi

django - 重命名 Django 父类(super class)模型并正确更新子类指针

转载 作者:行者123 更新时间:2023-12-03 15:09:50 24 4
gpt4 key购买 nike

我在 Django v2.2.12 中重构一个涉及三个模型的父类(super class)时遇到问题,一个父类(super class)模型和两个子类模型:

class BaseProduct(models.Model):
name = models.CharField()
description = models.CharField()


class GeneralProduct(BaseProduct):
pass


class SoftwareProduct(BaseProduct):
pass
BaseProduct模型需要重命名为 Product ,所以我将此代码更改为:

class Product(models.Model):
name = models.CharField()
description = models.CharField()

class GeneralProduct(Product):
pass


class SoftwareProduct(Product):
pass

然后跑 python manage.py makemigrations ,其中 Django 似乎正确地看到了发生了什么变化:

Did you rename the yourapp.BaseProduct model to Product? [y/N] y
Did you rename generalproduct.baseproduct_ptr to generalproduct.product_ptr (a OneToOneField)? [y/N] y
Did you rename softwareproduct.baseproduct_ptr to softwareproduct.product_ptr (a OneToOneField)? [y/N] y

Migrations for 'yourapp':
.../yourapp/migrations/002_auto_20200507_1830.py
- Rename model BaseProduct to Product
- Rename field baseproduct_ptr on generalproduct to product_ptr
- Rename field baseproduct_ptr on softwareproduct to product_ptr

到目前为止一切顺利。 Django 看到父类(super class)被重命名,它知道它自己自动生成的 ..._ptr它用于跟踪模型继承的值也需要在数据库中更新。

它提出的迁移结果看起来应该很简洁:

# Generated by Django 2.2.12 on 2020-05-07 18:30

from django.db import migrations


class Migration(migrations.Migration):

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

operations = [
migrations.RenameModel(
old_name='BaseProduct',
new_name='Product',
),
migrations.RenameField(
model_name='generalproduct',
old_name='baseproduct_ptr',
new_name='product_ptr',
),
migrations.RenameField(
model_name='softwareproduct',
old_name='baseproduct_ptr',
new_name='product_ptr',
),
]


这一切看起来都很完美,但使用 python manage.py migrate 应用该迁移崩溃了:

Running migrations:
Applying yourapp.0002_auto_20200507_1830...Traceback (most recent call last):
[...]
File ".../python3.7/site-packages/django/db/migrations/executor.py", line 245, in apply_migration
state = migration.apply(state, schema_editor)
File ".../python3.7/site-packages/django/db/migrations/migration.py", line 114, in apply
operation.state_forwards(self.app_label, project_state)
File ".../python3.7/site-packages/django/db/migrations/operations/models.py", line 340, in state_forwards
state.reload_models(to_reload, delay=True)
File ".../python3.7/site-packages/django/db/migrations/state.py", line 165, in reload_models
self._reload(related_models)
File ".../python3.7/site-packages/django/db/migrations/state.py", line 191, in _reload
self.apps.render_multiple(states_to_be_rendered)
File ".../python3.7/site-packages/django/db/migrations/state.py", line 308, in render_multiple
model.render(self)
File ".../python3.7/site-packages/django/db/migrations/state.py", line 579, in render
return type(self.name, bases, body)
File ".../python3.7/site-packages/django/db/models/base.py", line 253, in __new__
base.__name__,
django.core.exceptions.FieldError: Auto-generated field 'baseproduct_ptr' in class 'SoftwareProduct' for
parent_link to base class 'BaseProduct' clashes with declared field of the same name.

我在网上搜索了该错误,以及重命名作为其他模型父类(super class)的 Django 模型,但似乎没有任何(可发现的)文档、博客文章或 SO 答案讨论这个问题。

最佳答案

规范答案
出错的原因是即使 Django 看到模型被重命名并且子类需要指针更新,它也无法正确执行这些更新。在撰写本文时,有一个公关可以将其添加到 Django ( https://github.com/django/django/pull/13021 ,原为 11222 ),但在此之前,解决方案是暂时“欺骗” Django 使其认为子类实际上是没有任何继承的普通模型,并通过运行以下步骤手动实现更改:

  • 手动将自动生成的继承指针从 superclass_ptr 重命名为 newsuperclass_ptr(在这种情况下, baseproduct_ptr 变为 product_prt ),然后是
  • 通过为子类重写 .bases 属性并告诉 Django 重新加载它们,然后
  • 欺骗 Django 使其认为子类只是通用模型实现
  • 将父类(super class)重命名为其新名称(在本例中, BaseProduct 变为 Product ),最后是
  • 更新 newsuperclass_ptr 字段,使它们指向新的父类(super class)名称,确保指定 auto_created=Trueparent_link=True

  • 在最后一步中,第一个属性应该在那里,主要是因为 Django 自动生成指针,我们不希望 Django 能够告诉我们曾经欺骗过它并做了我们自己的事情,第二个属性在那里是因为 parent_link 是Django 在运行时正确连接模型继承所依赖的字段。
    因此,比 manage makemigrations 多几个步骤,但每个步骤都很简单,我们可以通过编写单个自定义迁移文件来完成所有这些。
    使用问题帖子中的名称:
    # Custom Django 2.2.12 migration for handling superclass model renaming.

    from django.db import migrations, models
    import django.db.models.deletion

    # with a file called custom_operations.py in our migrations dir:
    from .custom_operations import AlterModelBases


    class Migration(migrations.Migration):
    dependencies = [
    ('yourapp', '0001_initial'),
    # Note that if the last real migration starts with 0001,
    # this migration file has to start with 0002, etc.
    #
    # Django simply looks at the initial sequence number in
    # order to build its migration tree, so as long as we
    # name the file correctly, things just work.
    ]

    operations = [
    # Step 1: First, we rename the parent links in our
    # subclasses to match their future name:

    migrations.RenameField(
    model_name='generalproduct',
    old_name='baseproduct_ptr',
    new_name='product_ptr',
    ),

    migrations.RenameField(
    model_name='softwareproduct',
    old_name='baseproduct_ptr',
    new_name='product_ptr',
    ),

    # Step 2: then, temporarily set the base model for
    # our subclassses to just `Model`, which makes
    # Django think there are no parent links, which
    # means it won't try to apply crashing logic in step 3.

    AlterModelBases("GeneralProduct", (models.Model,)),
    AlterModelBases("SoftwareProduct", (models.Model,)),

    # Step 3: Now we can safely rename the superclass without
    # Django trying to fix subclass pointers:

    migrations.RenameModel(
    old_name="BaseProduct",
    new_name="Product"
    ),

    # Step 4: Which means we can now update the `parent_link`
    # fields for the subclasses: even though we altered
    # the model bases earlier, this step will restore
    # the class hierarchy we actually need:

    migrations.AlterField(
    model_name='generalproduct',
    name='product_ptr',
    field=models.OneToOneField(
    auto_created=True,
    on_delete=django.db.models.deletion.CASCADE,
    parent_link=True, primary_key=True,
    serialize=False,
    to='buyersguide.Product'
    ),
    ),

    migrations.AlterField(
    model_name='softwareproduct',
    name='product_ptr',
    field=models.OneToOneField(
    auto_created=True,
    on_delete=django.db.models.deletion.CASCADE,
    parent_link=True,
    primary_key=True,
    serialize=False,
    to='buyersguide.Product'
    ),
    ),
    ]
    关键的一步是继承“销毁”:我们告诉 Django 子类继承自 models.Model ,这样重命名父类(super class)将使子类完全不受影响(而不是 Django 尝试更新继承指针本身),但我们实际上并没有改变任何东西在数据库中。我们只对当前运行的代码进行更改,因此如果我们退出 Django,就好像从未进行过更改一样。
    因此,为了实现这一点,我们使用了一个自定义的 ModelOperation,它可以在运行时将一个(ny)类的继承更改为一个(一个或多个)不同父类(super class)的(ny 集合):
    # contents of yourapp/migrations/custom_operations.py

    from django.db.migrations.operations.models import ModelOperation


    class AlterModelBases(ModelOperation):
    reduce_to_sql = False
    reversible = True

    def __init__(self, name, bases):
    self.bases = bases
    super().__init__(name)

    def state_forwards(self, app_label, state):
    """
    Overwrite a models base classes with a custom list of
    bases instead, then force Django to reload the model
    with this (probably completely) different class hierarchy.
    """
    state.models[app_label, self.name_lower].bases = self.bases
    state.reload_model(app_label, self.name_lower)

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

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

    def describe(self):
    return "Update %s bases to %s" % (self.name, self.bases)
    有了这个自定义迁移文件和我们的 custom_operations.py,我们需要做的就是更新我们的代码以反射(reflect)新的命名方案:
    class Product(models.Model):
    name = models.CharField()
    description = models.CharField()

    class GeneralProduct(Product):
    pass


    class SoftwareProduct(Product):
    pass
    然后应用 manage migrate ,它将根据需要运行和更新所有内容。
    NOTE :取决于您是否“预先分解”了您的代码以准备重命名,使用以下内容:
    class BaseProduct(models.Model):
    name = models.CharField()
    description = models.CharField()


    # "handy" aliasing so that all code can start using `Product`
    # even though we haven't renamed actually renamed this class yet:
    Product = BaseProduct


    class GeneralProduct(Product):
    pass


    class SoftwareProduct(Product):
    pass
    您可能需要在其他类中将 ForeignKey 和 ManyToMany 关系更新为 Product,添加显式 add models.AlterField 指令以将 BaseProduct 更新为 Product:
            ...
    migrations.AlterField(
    model_name='productrating',
    name='product',
    field=models.ForeignKey(
    on_delete=django.db.models.deletion.CASCADE,
    to='yourapp.Product'
    ),
    ),
    ...

    原答案
    哦,是的,这是一个棘手的问题。但我已经在我的项目中解决了我这样做的方式。
    1) 删除新创建的迁移并回滚您的模型更改
    2) 使用 parent_link 选项将隐式父链接字段更改为显式。我们需要在后面的步骤中手动将我们的字段重命名为专有名称
    class BaseProduct(models.Model):
    ...

    class GeneralProduct(BaseProduct):
    baseproduct_ptr = models.OneToOneField(BaseProduct, django.db.models.deletion.CASCADE, parent_link=True, primary_key=True)

    class SoftwareProduct(BaseProduct):
    baseproduct_ptr = models.OneToOneField(BaseProduct, django.db.models.deletion.CASCADE, parent_link=True, primary_key=True)
    3) 通过 makemigrations 生成迁移并得到类似 的内容
    ...
    migrations.AlterField(
    model_name='generalproduct',
    name='baseproduct_ptr',
    field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='BaseProduct'),
    ),
    migrations.AlterField(
    model_name='softwareproduct',
    name='baseproduct_ptr',
    field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='BaseProduct'),
    )
    ...
    4) 现在您有了指向父模型的显式链接,您可以将它们重命名为 product_ptr,这将匹配您想要的链接名称
    class GeneralProduct(BaseProduct):
    product_ptr = models.OneToOneField(BaseProduct, django.db.models.deletion.CASCADE, parent_link=True, primary_key=True)

    class SoftwareProduct(BaseProduct):
    product_ptr = models.OneToOneField(BaseProduct, django.db.models.deletion.CASCADE, parent_link=True, primary_key=True)
    5) 通过 makemigrations 生成迁移并得到类似 的内容
    ...
    migrations.RenameField(
    model_name='generalproduct',
    old_name='baseproduct_ptr',
    new_name='product_ptr',
    ),
    migrations.RenameField(
    model_name='softwareproduct',
    old_name='baseproduct_ptr',
    new_name='product_ptr',
    ),
    ...
    6)现在最棘手的部分是我们需要添加新的迁移操作(源可以在这里找到 https://github.com/django/django/pull/11222 )并放入我们的代码中,我个人在我的项目中有 contrib 包,我把所有员工都这样
    文件在 contrib/django/migrations.py
    # https://github.com/django/django/pull/11222/files
    # https://code.djangoproject.com/ticket/26488
    # https://code.djangoproject.com/ticket/23521
    # https://code.djangoproject.com/ticket/26488#comment:18
    # https://github.com/django/django/pull/11222#pullrequestreview-233821387
    from django.db.migrations.operations.models import ModelOperation


    class DisconnectModelBases(ModelOperation):
    reduce_to_sql = False
    reversible = True

    def __init__(self, name, bases):
    self.bases = bases
    super().__init__(name)

    def state_forwards(self, app_label, state):
    state.models[app_label, self.name_lower].bases = self.bases
    state.reload_model(app_label, self.name_lower)

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

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

    def describe(self):
    return "Update %s bases to %s" % (self.name, self.bases)
    7) 现在我们准备重命名我们的父模型
    class Product(models.Model):
    ....

    class GeneralProduct(Product):
    pass


    class SoftwareProduct(Product):
    pass
    8) 通过 makemigrations 生成迁移。确保添加 DisconnectModelBases 步骤,它不会自动添加,即使成功生成迁移。如果这没有帮助,您可以尝试手动创建 --empty
    from django.db import migrations, models
    import django.db.models.deletion

    from contrib.django.migrations import DisconnectModelBases


    class Migration(migrations.Migration):

    dependencies = [
    ("contenttypes", "0002_remove_content_type_name"),
    ("products", "0071_auto_20200122_0614"),
    ]

    operations = [
    DisconnectModelBases("GeneralProduct", (models.Model,)),
    DisconnectModelBases("SoftwareProduct", (models.Model,)),
    migrations.RenameModel(
    old_name="BaseProduct", new_name="Product"
    ),
    migrations.AlterField(
    model_name='generalproduct',
    name='product_ptr',
    field=models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='products.Product'),
    ),
    migrations.AlterField(
    model_name='softwareproduct',
    name='product_ptr',
    field=models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='proudcts.Product'),
    ),
    ]
    注意:毕竟,您不需要明确的 parent_link 字段。所以你可以删除它们。我实际上在第 7 步中做了。

    关于django - 重命名 Django 父类(super class)模型并正确更新子类指针,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61665607/

    24 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com