Prechádzať zdrojové kódy

Clarified SeparateDatabaseAndState docs and added example of changing ManyToManyField.

Co-Authored-By: Mariusz Felisiak <felisiak.mariusz@gmail.com>
Co-Authored-By: Carlton Gibson <carlton.gibson@noumenal.es>
Co-Authored-By: René Fleschenberg <rene@fleschenberg.net>
Adam Johnson 5 rokov pred
rodič
commit
a9ee6872bd

+ 86 - 0
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
 ======================================
 

+ 16 - 4
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<makemigrations --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: