Home > Back-end >  How to transition from M2M created with 'through' to a M2M managed by Django
How to transition from M2M created with 'through' to a M2M managed by Django

Time:11-14

I'm working in a Project where there were multiple fields created as M2M using the through related model, however they were wrongly created by then and the related models have no additional fields, so I would like to use a regular M2M managed by Django.

The existing models I have are:

cass Student(models.Model):
    # ... other fields..."
    courses = models.ManyToManyField(
        Course,
        related_name='students',
        through='StudentCourse'
    )

class StudentCourse(models.Model):
    student = models.ForeignKey(Student, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, on_delete=models.CASCADE)

cass Course(models.Model):
    # ... other fields..."

And I would like to have:

class Student(models.Model):
    # ... other fields..."
    course = models.ManyToManyField(Course, related_name='students')

class Course(models.Model):
    # ... other fields..."

I'm not able to find a way in Django to do this without losing the data that was already inserted.

I was thinking of renaming the tables in the same way Django does and manipulate the Django migration metadata but it is an ugly way to solve it.

Is there a Django way to solve this without losing the data (or creating backup tables)?

CodePudding user response:

To do this you need to create a migration file that copies the data from the old through table to the new one.

First create an empty migration file that will be populated with our migration operations

python manage.py makemigrations <app> --empty

Then that migration needs to be populated with the following (<app> should be replaced with the app name of the app)

from django.db import migrations, models


def move_courses(apps, schema_editor):
    Student = apps.get_model('<app>', 'Student')
    StudentCourse = apps.get_model('<app>', 'StudentCourse')
    for student in Student.objects.all():
        student.courses.set(
            StudentCourse.objects.filter(student=student).values_list('course', flat=True)
        )


class Migration(migrations.Migration):

    dependencies = [
        ('<app>', '<XXXX_previous_migration_name>'),
    ]

    operations = [
        # Remove the old ManyToManyField
        # This won't delete the through table or it's data
        migrations.RemoveField(
            model_name='Student',
            name='courses',
        ),
        # Add the new ManyToManyField
        migrations.AddField(
            model_name='Student',
            name='courses',
            field=models.ManyToManyField(related_name='students', to='<app>.Course'),
        ),
        # Run a script that copies data from the old through table to the new one
        migrations.RunPython(move_courses),
        # Delete the old through table
        migrations.DeleteModel(
            name='StudentCourse',
        ),
    ]

Then update your models in the way you like:

class Student(models.Model):
    # ... other fields..."
    courses = models.ManyToManyField(
        Course,
        related_name='students',
    )

# Since through='StudentCourse' was removed, below is removed to (keeping it as comment to represent the removal)
# class StudentCourse(models.Model):
#    student = models.ForeignKey(Student, on_delete=models.CASCADE)
#    course = models.ForeignKey(Course, on_delete=models.CASCADE)

class Course(models.Model):
    # ... other fields..."

After models and the empty created migration is updated, you can run migrate

python manage.py migrate

Important: don't run makemigrations again, only migrate in order for Django to run the migration that moves the data.

  • Related