简体   繁体   中英

Migrate from multi-table inheritance model to abstract base classes in Django

My current project uses multi-table inheritance models :

from django.db import models

class Place(models.Model):
    name = models.CharField(max_length=50)
    address = models.CharField(max_length=80)

class Restaurant(Place):
    serves_hot_dogs = models.BooleanField(default=False)
    serves_pizza = models.BooleanField(default=False)

class Cinema(Place):
    sells_tickets = models.BooleanField(default=False)
    sells_popcorn = models.BooleanField(default=False)

I want to switch to abstract base classes instead. Since my model is already deployed I need to write some custom migrations to convert the above schema to this one:

from django.db import models

class AbstractPlace(models.Model):
    name = models.CharField(max_length=50)
    address = models.CharField(max_length=80)

    class Meta:
        abstract = True

class Restaurant(AbstractPlace):
    serves_hot_dogs = models.BooleanField(default=False)
    serves_pizza = models.BooleanField(default=False)

class Cinema(AbstractPlace):
    sells_tickets = models.BooleanField(default=False)
    sells_popcorn = models.BooleanField(default=False)

Does anyone have any advice on the steps to take to achieve this?

I recently tackled this exact problem, which I solved by writing and running the migration in the code block below - loosely translated to fit the models in your case.

I'm pretty sure that it's not possible to alter the tables of the old Restaurant and Cinema models directly, as if you try to add fields to them, they will collide with the existing fields of the base model, and if you try to "decouple" the derived models from the base model by eg by manually setting abstract=True in the base model's options , Django reports that it's unable to find the base models of Restaurant and Cinema . (These issues might be caused by a bug, for all I know.) To circumvent this problem, I created new tables for the derived models, copied the data from the old tables to the new ones, deleted the old tables, and renamed the new tables to match the names of the old ones.

I got large parts of the code below from code generated by Django, which can be reproduced by creating a temporary migration (before creating one with the code below) which only deletes Restaurant , Cinema and Place , running makemigrations , and copying the CreateModel() s and AlterField() s (for related fields pointing to Restaurant or Cinema ) from the generated migration.

For the record, I'm using Django 3.1.4.

from django.db import migrations, models


def copy_objects_from_restaurant_and_cinema_to_restaurant_tmp_and_cinema_tmp(apps, schema_editor):
    Restaurant_Tmp = apps.get_model('<app name>', 'Restaurant_Tmp')
    Cinema_Tmp = apps.get_model('<app name>', 'Cinema_Tmp')
    Restaurant = apps.get_model('<app name>', 'Restaurant')
    Cinema = apps.get_model('<app name>', 'Cinema')
    # The `_meta.fields` list includes the PK
    copy_objects_from_old_model_to_new_model(Restaurant, Restaurant_Tmp, Restaurant_Tmp._meta.fields)
    copy_objects_from_old_model_to_new_model(Cinema, Cinema_Tmp, Cinema_Tmp._meta.fields)


def copy_objects_from_old_model_to_new_model(old_model, new_model, fields_to_copy):
    field_names = [field.name for field in fields_to_copy]
    for old_obj in old_model.objects.all():
        old_obj_field_dict = {
            field_name: getattr(old_obj, field_name)
            for field_name in field_names
        }
        new_model.objects.create(**old_obj_field_dict)


def copy_objects_from_restaurant_tmp_and_cinema_tmp_to_restaurant_and_cinema(apps, schema_editor):
    Restaurant_Tmp = apps.get_model('<app name>', 'Restaurant_Tmp')
    Cinema_Tmp = apps.get_model('<app name>', 'Cinema_Tmp')
    Restaurant = apps.get_model('<app name>', 'Restaurant')
    Cinema = apps.get_model('<app name>', 'Cinema')
    copy_objects_from_old_model_to_new_model(Restaurant_Tmp, Restaurant, Restaurant_Tmp._meta.fields)
    copy_objects_from_old_model_to_new_model(Cinema_Tmp, Cinema, Cinema_Tmp._meta.fields)


class Migration(migrations.Migration):

    dependencies = [
        ('<app name>', '<last migration>'),
    ]

    operations = [
        migrations.CreateModel(
            name='Restaurant_Tmp',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=50)),
                ('address', models.CharField(max_length=80)),
                ('serves_hot_dogs', models.BooleanField(default=False)),
                ('serves_pizza', models.BooleanField(default=False)),
            ],
            options={
                'abstract': False,
            },
        ),
        migrations.CreateModel(
            name='Cinema_Tmp',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=50)),
                ('address', models.CharField(max_length=80)),
                ('sells_tickets', models.BooleanField(default=False)),
                ('sells_popcorn', models.BooleanField(default=False)),
            ],
            options={
                'abstract': False,
            },
        ),
        migrations.RunPython(copy_objects_from_restaurant_and_cinema_to_restaurant_tmp_and_cinema_tmp, migrations.RunPython.noop),

        # Update foreign keys to reference the non-abstract models directly,
        # instead of through the (automatically generated) `place_ptr` field of the old models
        < 
          Run `migrations.AlterField()` here for each related field (like ForeignKey) of other models that point to Restaurant or Cinema,
          but change their `to` argument from e.g. `<app name>.restaurant` to `<app name>.restaurant_tmp`
        >
        migrations.RunPython(migrations.RunPython.noop, copy_objects_from_restaurant_tmp_and_cinema_tmp_to_restaurant_and_cinema),

        migrations.DeleteModel(
            name='Restaurant',
        ),
        migrations.DeleteModel(
            name='Cinema',
        ),
        migrations.DeleteModel(
            name='Place',
        ),
        migrations.RenameModel(
            old_name='Restaurant_Tmp',
            new_name='Restaurant',
        ),
        migrations.RenameModel(
            old_name='Cinema_Tmp',
            new_name='Cinema',
        ),
    ]

Note that the migration I originally wrote was only tested to work using SQLite; other database management systems might not accept such a large variety of migration operations, and you might have to split it into multiple migrations. (I'm somewhat unsure what exactly could cause this problem, but I can recall that I've experienced it with PostgreSQL.)

Please let me know if this solves your problem!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM