Канонический ответ
Причина, по которой это идет не так, заключается в том, что хотя Django видит , что модель была переименована, а подклассы нуждаются в обновлении указателя, она не может правильно выполнить в этих обновлениях. На момент написания (https://github.com/django/django/pull/11222) существует PR, чтобы добавить это к Django, но до тех пор, пока это не произойдет, решение состоит в том, чтобы временно «обмануть» Django, заставив думать о подклассах на самом деле являются простыми моделями без какого-либо наследования и вносят изменения вручную, выполнив следующие шаги:
- переименование автоматически сгенерированного указателя наследования с
superclass_ptr
на newsuperclass_ptr
вручную (в данном случае baseproduct_ptr
становится product_prt
), затем - трюк Django, заставляющий думать, что подклассы являются просто общими c реализациями модели, буквально переписывая для них свойство
.bases
и сообщая Django перезагрузить их, затем - переименование суперкласса в его новое имя (в этом случае
BaseProduct
становится Product
) и, наконец, - обновление полей
newsuperclass_ptr
, чтобы они указывали на новый суперкласс вместо этого укажите auto_created=True
, а также parent_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, это будет выглядеть так, как будто это изменение никогда не было сделано с самого начала.
Итак, чтобы добиться этого, мы используем custom 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
в других классах, добавьте явные инструкции 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 . Нам нужно это, чтобы вручную переименовать наше поле в имя propper на более поздних этапах
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.