Changing a ManyToManyField to use a through model?

Hi all.
I feel puzzled when I see the code below.

Why is it recommended to change the table name?

I tried to delete the database_operations in the following code, and then change the table name of AuthorBook to core_book_authors in state_operations.

It can still migrate successfully and add fields to the intermediate table.

Why write a RunSQL to change the table name?

Does that have any special effects?

This is a link to this document. Writing database migrations | Django documentation | Django (djangoproject.com)

I posted three pieces of code.(official code,my modification and my model)

If you have any ideas, welcome to reply, thank you for your answer.

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
    dependencies = [
        ('core', '0001_initial'),
    ]

    operations = [
        migrations.SeparateDatabaseAndState(
            database_operations=[
                # Old table name from checking with sqlmigrate, new table
                # name from AuthorBook._meta.db_table.
                migrations.RunSQL(
                    sql='ALTER TABLE core_book_authors RENAME TO core_authorbook',
                    reverse_sql='ALTER TABLE core_authorbook RENAME TO core_book_authors',
                ),
            ],
            state_operations=[
                migrations.CreateModel(
                    name='AuthorBook',
                    fields=[
                        (
                            'id',
                            models.AutoField(
                                auto_created=True,
                                primary_key=True,
                                serialize=False,
                                verbose_name='ID',
                            ),
                        ),
                        (
                            'author',
                            models.ForeignKey(
                                on_delete=django.db.models.deletion.DO_NOTHING,
                                to='core.Author',
                            ),
                        ),
                        (
                            'book',
                            models.ForeignKey(
                                on_delete=django.db.models.deletion.DO_NOTHING,
                                to='core.Book',
                            ),
                        ),
                    ],
                ),
                migrations.AlterField(
                    model_name='book',
                    name='authors',
                    field=models.ManyToManyField(
                        to='core.Author',
                        through='core.AuthorBook',
                    ),
                ),
            ],
        ),
        migrations.AddField(
            model_name='authorbook',
            name='is_primary',
            field=models.BooleanField(default=False),
        ),
    ]

You can compare with the following code.

# Generated by Django 3.2.8 on 2021-11-19 13:35
import django
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('mysite', '0002_author_book'),
    ]

    operations = [
        migrations.SeparateDatabaseAndState(
            database_operations=[],
            state_operations=[
                migrations.CreateModel(
                    name='MysiteAuthorBook',
                    fields=[
                        ('id',
                         models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                        ('author',
                         models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='mysite.author')),
                        ('book', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='mysite.book')),
                    ],
                    options={
                        'db_table': 'mysite_author_book',
                        'unique_together': {('author', 'book')},
                    },
                ),
                migrations.AlterField(
                    model_name='book',
                    name='author',
                    field=models.ManyToManyField(blank=True, related_name='authors', through='mysite.MysiteAuthorBook',
                                                 to='mysite.Author'),
                ),
            ],
        ),
    ]

models.py

class Book(models.Model):
    name = models.CharField(max_length=64)
    author = models.ManyToManyField('Author', related_name='authors', blank=True, through='MysiteAuthorBook')


class Author(models.Model):
    name = models.CharField(max_length=64)


class MysiteAuthorBook(models.Model):
    author = models.ForeignKey('Author', models.DO_NOTHING)
    book = models.ForeignKey('Book', models.DO_NOTHING)

    class Meta:
        db_table = 'mysite_author_book'
        unique_together = (('author', 'book'),)

The very first sentence answers that:

If you change a ManyToManyField to use a through model, the default migration will delete the existing table and create a new one, losing the existing relations.

If you want to keep any existing relationships, you need to do some extra work - one such option is as they describe. (There are other ways of handling this situation - it’s not implied here that you must do it this way. But the documented method works.)

I wrote that section of the docs.

Ken is right that the motivation for using SeparateDatabaseAndState is to keep the existing table.

The reason the table is renamed is to match Django’s autogenerated table name for the model. For M2M models, Django generates a name like <app>_<source>_<dest> (core_author_book). But for a custom through model called <source><dest> (like AuthorBook), <app>_<modelname> (core_authorbook). Hence the suggested rename in that section.

Since that doc section focusses on how to use SeparateDatabaseAndState, and not the exact reasoning why, I didn’t explain it much. But there’s this comment that explains the sources of the table names:

# Old table name from checking with sqlmigrate, new table
# name from AuthorBook._meta.db_table.

It is possible to keep the table name the same, by setting Meta.db_table. But I always find it a bit weird when models have non-default table names, as it can make it harder to navigate the database.

Hope that helps.

We could add a sentence to that docs section explaining it, maybe?