Django migration for through model to a ManyToMany
I recently had the problem that a m2n relation in a django application should be sortable. So I wanted to add a through model to the relation but unfortunately as of now (django 2.0) django won’t solve this migration on its own.
These were the steps I have done to accomplish this:
- Add a new m2n relation with the through model
- Build a migration and extend it with orm code that copies the existing data
- Delete the old relation
- Rename the new relation
Add the new relation
Create the through model:
class ThroughModel(models.Model): left = models.ForeignKey('Left') right = models.ForeignKey('Right') order = models.PositiveSmallIntegerField() class Meta: ordering = ['order', ]
Go to the place where you defined your old
ManyToManyField. It might look something like this:
class Left(models.Model): ... the_old_relation = models.ManyToManyField( 'Right', blank=True, related_name='lefts' )
And replace it with new one:
the_new_relation = models.ManyToManyField( 'Right', blank=True, through='ThroughModel', related_name='new_lefts' )
Keep an eye on the
related_name. It should not be the same.
Build a migration and extend it manually
python manage.py makemigrations and open the newly generated migration.
It will contains something like this:
from django.db import migrations, models # space for two methods class Migration(migrations.Migration): dependencies = [ ('test', '00...'), ] operations = [ migrations.CreateModel( name='ThroughModel', ... ), migrations.AddField( model_name='left', name='the_new_relation', field=models.ManyToManyField(blank=True, related_name='new_lefts', to='test.Right', through='test.ThroughModel'), ), # ToDo: Add RunPython here # ToDo: Add RemoveField here # ToDo: Add RenameField here ]
So far so good. Let’s tackle the ToDos that I added to the code.
ToDo: Add RunPython
The RunPython part will take care of the old relation. We cannot loose them. There would have been the option to do this in SQL but django ORM will be alright for this small piece of migration.
We have to write two methods. One to apply the migrations (
forward_func) and the other one to roll them back (
The code is generic. If you have the same problem you can use it, replace
right with your models and you should be good to go.
def forwards_func(apps, schema_editor): # We get the model from the versioned app registry; # if we directly import it, it'll be the wrong version Left = apps.get_model("test", "Left") ThroughModel = apps.get_model("test", "ThroughModel") db_alias = schema_editor.connection.alias for left in Left.objects.using(db_alias).iterator(): for right in left.rights.all(): ThroughModel.objects.create(left=left, right=right, order=0) def reverse_func(apps, schema_editor): ThroughModel = apps.get_model("test", "ThroughModel") db_alias = schema_editor.connection.alias for througModel in ThroughModel.objects.using(db_alias).iterator(): left = throughModel.left right = throughModel.right left.the_old_relation.add(right)
You can replace the comment
# space for two methods with the methods.
And then we replace the
# ToDo: Add RunPython here with
migrations.RunPython(forwards_func, reverse_func), in order to add the execution of the code to the migration.
# ToDo: Add RemoveField After copying over the data we can remove the old field by replacing the corresponding todo comment with the following snippet:
migrations.RemoveField( model_name='Left', name='the_old_relation', ),
# ToDo: Add RenameField
Lastly I rename the new field to the old field’s name in order to avoid changes in the code.
migrations.RenameField( 'left', # model 'the_old_relation', # old field name 'the_new_relation' # new field name )
(Same procedure with the
# ToDo: Add RenameField here)