【问题标题】:Renaming a Django superclass model and updating the subclass pointers correctly重命名 Django 超类模型并正确更新子类指针
【发布时间】:2020-08-23 05:03:24
【问题描述】:

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

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 看到超类被重命名,它知道它自己用于跟踪模型继承的自动生成的..._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.

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

【问题讨论】:

  • 您也介意添加相关字段吗? (也许这会帮助我们重现行为)
  • 仅供参考: 我创建了一个类似的场景,但它没有抛出任何错误。所以,很高兴添加 "minimal reproducable" 代码示例
  • @ArakkalAbu 这 MRE...向BaseProduct 模型添加任意字段以避免错误。
  • 我不认为您的示例是 MRE 示例,(我在此配置中成功运行迁移,BeforeAfter
  • 请注意,BaseProduct 不是抽象的。任何字段都应该没问题,但是Meta 可以改变 Django 所做的事情,所以尽量避开它。另外,不要添加该指针:您仍然显示带有 Django 管理的字段的代码。您永远不会添加它们,它们只是存在:通过使它们成为Charfield,您完全否定了这个问题的含义。我添加了一些字段,这样您就可以不再对所问的内容感到困惑:Django makes superclassname_ptr fields to track model inheritance,问题是当 it 时会发生什么i> 编写代码来改变这些。

标签: django inheritance django-migrations


【解决方案1】:

规范答案

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

  1. 手动将自动生成的继承指针从 superclass_ptr 重命名为 newsuperclass_ptr(在这种情况下,baseproduct_ptr 变为 product_prt),然后
  2. 通过为子类重写 .bases 属性并告诉 Django 重新加载它们,让 Django 认为子类只是通用模型实现,然后
  3. 将超类重命名为新名称(在这种情况下,BaseProduct 变为 Product),然后最后
  4. 更新newsuperclass_ptr 字段,使其指向新的超类名称,确保指定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,因此重命名超类将使子类完全不受影响(而不是 Django 尝试更新继承指针本身),但我们实际上并没有更改数据库中的任何内容。我们只对当前正在运行的代码进行更改,因此如果我们退出 Django,就好像从未进行过更改一样。

因此,为了实现这一点,我们使用了一个自定义的ModelOperation,它可以在运行时将一个(ny)类的继承更改为一个(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,我们需要做的就是更新我们的代码以反映新的命名方案:

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

class GeneralProduct(Product):
    pass


class SoftwareProduct(Product):
    pass

然后应用manage migrate,它将根据需要运行并更新所有内容。

注意:取决于您是否“预先构造”了代码以准备重命名,使用如下方式:

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,添加显式添加 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 选项将隐式父链接字段更改为显式。我们需要这个在后面的步骤中手动将我们的字段重命名为propper name

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 步中完成的。

【讨论】:

  • 现在尝试这个,所以有几个问题:你能解释为什么AlterModelBases 必须在超类重命名之前出现吗? (特别是为了确保作为规范答案,该信息被捕获)。另外,您能否解释一下(可能是在代码 cmets 中?)为什么 AlterModelBases.state_forward 有效?
  • 另外,我采用了您的方法并将其简化为一个包含四个步骤的迁移文件,从而避免了使父类 ptr 字段显式的需要,所以我想编辑这个答案在将其标记为正确答案之前先位,只是为了确保我们有一个由尽可能少的步骤组成的规范答案。
  • 您可能还注意到,对于除 SQLite example 这样的 migrations.RenameModel 对象的覆盖 state_forwards 方法在 django 2.2.12 中按预期工作。
  • 谢谢,很高兴为您提供帮助))如果没有锥形解决方案,此问题将花费任何开发人员太多时间。如果我记得在我的项目中需要这个解决方案时,我自己会花费 2 或 3 天时间来寻找解决方案。
  • 不开玩笑,我在这个问题上卡住了 2 天,然后才去“这很愚蠢,我已经有太多的代表了,而且有人必须已经解决了这个问题”。感谢您找到这个并愿意帮助每个人得到一个好的答案来参考!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-11-01
  • 2018-12-04
  • 1970-01-01
  • 1970-01-01
  • 2015-10-23
  • 2011-10-16
  • 2020-08-25
相关资源
最近更新 更多