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>
This commit is contained in:
Adam Johnson 2020-03-03 17:51:39 +00:00 committed by Mariusz Felisiak
parent a2f554249e
commit a9ee6872bd
2 changed files with 102 additions and 4 deletions

View File

@ -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 data from the new application. Adjust the second argument of the
:mod:`~django.db.migrations.operations.RunPython` operation accordingly. :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 Changing an unmanaged model to managed
====================================== ======================================

View File

@ -419,12 +419,24 @@ if ``atomic=True`` is passed to the ``RunPython`` operation.
.. class:: SeparateDatabaseAndState(database_operations=None, state_operations=None) .. 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. (schema-changing) and state (autodetector-powering) aspects of operations.
It accepts two lists of operations, and when asked to apply state will use the It accepts two lists of operations. When asked to apply state, it will use the
state list, and when asked to apply changes to the database will use the database ``state_operations`` list (this is a generalized version of :class:`RunSQL`'s
list. Do not use this operation unless you're very sure you know what you're doing. ``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: .. _writing-your-own-migration-operation: