#36959 closed Bug (duplicate)
Model bases isn't updated when changing parent classes
| Reported by: | Timothy Schilling | Owned by: | |
|---|---|---|---|
| Component: | Migrations | Version: | |
| Severity: | Normal | Keywords: | mti |
| Cc: | Triage Stage: | Unreviewed | |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
There appears to be a bug when removing multi-table inheritance. The majority of this ticket is covered in this blog post, but I'm pulling out the relevant bits. If things don't make total sense, it's possible I removed too much context.
When removing multi-table inheritance by changing a child model's parent from a concrete model to an abstract model (or models.Model directly), makemigrations does not generate a migration operation to update the bases attribute in the migration state.
Steps to reproduce:
- Start with these models
class MyBaseModel(models.Model):
value = models.IntegerField()
class MyChildModel(MyBaseModel):
pass
Make migrations and apply them.
- Change the models to:
class AbstractBase(models.Model):
class Meta:
abstract = True
value = models.IntegerField(null=True)
class MyBaseModel(models.Model):
pass
class MyChildModel(AbstractBase):
pass
Make migrations and apply them. You'll get the error:
django.core.exceptions.FieldError: Local field 'id' in class 'MyChildModel' clashes with field of the same name from base class 'MyBaseModel'.
This is because there is no operation to update bases for MyChildModel from ("myapp.mybasemodel",) to (models.Model,).
Expected Behavior
makemigrations should detect that a model's bases have changed and generate an appropriate migration operation (similar to AlterModelOptionsmaybe?) to update bases.
Workaround
Thanks to Andrii on StackOverflow, there's a workaround for people to create their own operation to change the bases.
from django.db.migrations.operations.models import ModelOptionOperation
class SetModelBasesOptionOperation(ModelOptionOperation):
"""
A migration operation that updates the bases of a model.
This can be used to separate a model from its parent. Specifically
when multi-table inheritance is used.
"""
def __init__(self, name, bases):
super().__init__(name)
self.bases = bases
def deconstruct(self):
return (self.__class__.__qualname__, [], {"bases": self.bases})
def state_forwards(self, app_label, state):
model_state = state.models[app_label, self.name_lower]
model_state.bases = self.bases
state.reload_model(app_label, self.name_lower, delay=True)
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 bases of the model %s" % self.name
@property
def migration_name_fragment(self):
return "set_%s_bases" % self.name_lower
Which would require the earlier migration's operations that fails to be updated to look like:
operations = [
migrations.RemoveField(
model_name='mybasemodel',
name='value',
),
migrations.RemoveField(
model_name='mychildmodel',
name='mybasemodel_ptr',
),
SetModelBasesOptionOperation("mychildmodel", (models.Model, )),
migrations.AddField(
model_name='mychildmodel',
name='id',
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AddField(
model_name='mychildmodel',
name='value',
field=models.IntegerField(null=True),
),
]
Change History (2)
comment:1 by , 76 minutes ago
| Keywords: | mti added |
|---|---|
| Resolution: | → duplicate |
| Status: | new → closed |
| Type: | Uncategorized → Bug |
Thanks for the clear write-up. I think it can be handled as part of #23521.