pythondjangomodelmigrationinherited

Django: outsource model properties with inheritance to a more general model


I have noticed, that I need a generalized model based on a specified model, following example should show what I mean:

before:

class TextResult(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, default=1)
    text = models.ForeignKey(Text)
    wpm = models.FloatField(default=0.0)
    accuracy = models.FloatField(default=1.0)

after:

class TypingResult(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, default=1)
    wpm = models.FloatField(default=0.0)
    accuracy = models.FloatField(default=1.0)


class TextResult(TypingResult):
    text = models.ForeignKey(Text)

Though there is already some data in the original model, so it is necessary to migrate the data to the new modelstructure


Solution

  • The following answer is based on this answer(https://stackoverflow.com/a/44148102/4129587)

    In order to achieve that it is necessary to do a manual Data Migration

    The following 5 basic migration steps lead to the desired result:

    1. create the new model TypingResult
    2. create a new foreign key that is nullable to the new model TypingResult in the old model TextResult
    3. copy all the old attributes to a new instance of the new model TypingResult
    4. remove the old attributes including the id from the original model
    5. alter the foreign key as the new primary key

    It might be possible to start the migration with an autogenerated migration of the new models

    The following code is based on an automatically generated migration and already tested

    from __future__ import unicode_literals
    
    from django.conf import settings
    import django.core.validators
    from django.db import migrations, models
    import django.db.models.deletion
    
    def copy_text_results_to_typing_results(apps, schema_editor):
        TypingResult = apps.get_model('testapp', 'TypingResult')
        TextResult = apps.get_model('testapp', 'TextResult')
        for text_result in TextResult.objects.all():
            copied_result = TypingResult()
            copied_result.user = text_result.user
            copied_result.wpm = text_result.wpm
            copied_result.accuracy = text_result.accuracy
            copied_result.save()
            text_result.typingresult_ptr = copied_result
            text_result.save()
    
    class Migration(migrations.Migration):
    
        dependencies = [
            migrations.swappable_dependency(settings.AUTH_USER_MODEL),
            ('testapp', '0001_initial'),
        ]
    
        operations = [
            migrations.CreateModel(
                name='TypingResult',
                fields=[
                    ('id', models.AutoField(auto_created=True, 
                                            primary_key=True,
                                            serialize=False,
                                            verbose_name='ID')),
                    ('wpm', models.FloatField(default=0.0)),
                    ('accuracy', models.FloatField(default=1.0)),
                    ('user', models.ForeignKey(default=1, 
                                               on_delete=django.db.models.deletion.CASCADE,
                                               to=settings.AUTH_USER_MODEL)),
                ],
            ),
            # add the foreign key for the new inherited model,
            # it is allowed to have null values since the actual values have to be
            # copied first to this, it will be changed later
            migrations.AddField(
                model_name='textresult',
                name='typingresult_ptr',
                field=models.OneToOneField(blank=True, null=True, to='testapp.TypingResult'),
            ),
            # copy the old values to the new inherited model
            migrations.RunPython(copy_text_results_to_typing_results),
            # remove the old id and the copied fields from the TextResult model
            migrations.RemoveField(
                model_name='textresult',
                name='accuracy',
            ),
            migrations.RemoveField(
                model_name='textresult',
                name='id',
            ),
            migrations.RemoveField(
                model_name='textresult',
                name='user',
            ),
            migrations.RemoveField(
                model_name='textresult',
                name='wpm',
            ),
            # alter the id of the inherited model to be the new primary key
            migrations.AlterField(
                model_name='textresult',
                name='typingresult_ptr',
                field=models.OneToOneField(auto_created=True,
                                           on_delete=django.db.models.deletion.CASCADE,
                                           parent_link=True,
                                           primary_key=True,
                                           serialize=False,
                                           to='testapp.TypingResult'),
            ),
        ]