From a9ee6872bd9e1bacc2da827dbd5b9093f724e4a5 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 3 Mar 2020 17:51:39 +0000 Subject: [PATCH] Clarified SeparateDatabaseAndState docs and added example of changing ManyToManyField. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Mariusz Felisiak Co-Authored-By: Carlton Gibson Co-Authored-By: René Fleschenberg --- docs/howto/writing-migrations.txt | 86 +++++++++++++++++++++++++++++++ docs/ref/migration-operations.txt | 20 +++++-- 2 files changed, 102 insertions(+), 4 deletions(-) diff --git a/docs/howto/writing-migrations.txt b/docs/howto/writing-migrations.txt index 084513b312..ab1a897aa0 100644 --- a/docs/howto/writing-migrations.txt +++ b/docs/howto/writing-migrations.txt @@ -318,6 +318,92 @@ could either do nothing (as in the example above) or remove some or all of the data from the new application. Adjust the second argument of the :mod:`~django.db.migrations.operations.RunPython` operation accordingly. +.. _changing-a-manytomanyfield-to-use-a-through-model: + +Changing a ``ManyToManyField`` to use a ``through`` model +========================================================= + +If you change a :class:`~django.db.models.ManyToManyField` to use a ``through`` +model, the default migration will delete the existing table and create a new +one, losing the existing relations. To avoid this, you can use +:class:`.SeparateDatabaseAndState` to rename the existing table to the new +table name whilst telling the migration autodetector that the new model has +been created. You can check the existing table name through +:djadmin:`sqlmigrate` or :djadmin:`dbshell`. You can check the new table name +with the through model's ``_meta.db_table`` property. Your new ``through`` +model should use the same names for the ``ForeignKey``\s as Django did. Also if +it needs any extra fields, they should be added in operations after +:class:`.SeparateDatabaseAndState`. + +For example, if we had a ``Book`` model with a ``ManyToManyField`` linking to +``Author``, we could add a through model ``AuthorBook`` with a new field +``is_primary``, like so:: + + 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), + ), + ] + Changing an unmanaged model to managed ====================================== diff --git a/docs/ref/migration-operations.txt b/docs/ref/migration-operations.txt index 148ac95839..9723478032 100644 --- a/docs/ref/migration-operations.txt +++ b/docs/ref/migration-operations.txt @@ -419,12 +419,24 @@ if ``atomic=True`` is passed to the ``RunPython`` operation. .. class:: SeparateDatabaseAndState(database_operations=None, state_operations=None) -A highly specialized operation that let you mix and match the database +A highly specialized operation that lets you mix and match the database (schema-changing) and state (autodetector-powering) aspects of operations. -It accepts two lists of operations, and when asked to apply state will use the -state list, and when asked to apply changes to the database will use the database -list. Do not use this operation unless you're very sure you know what you're doing. +It accepts two lists of operations. When asked to apply state, it will use the +``state_operations`` list (this is a generalized version of :class:`RunSQL`'s +``state_operations`` argument). When asked to apply changes to the database, it +will use the ``database_operations`` list. + +If the actual state of the database and Django's view of the state get out of +sync, this can break the migration framework, even leading to data loss. It's +worth exercising caution and checking your database and state operations +carefully. You can use :djadmin:`sqlmigrate` and :djadmin:`dbshell` to check +your database operations. You can use :djadmin:`makemigrations`, especially +with :option:`--dry-run`, to check your state +operations. + +For an example using ``SeparateDatabaseAndState``, see +:ref:`changing-a-manytomanyfield-to-use-a-through-model`. .. _writing-your-own-migration-operation: