Skip to content

Commit

Permalink
[3.0.x] Clarified SeparateDatabaseAndState docs and added example of …
Browse files Browse the repository at this point in the history
…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>

Backport of a9ee687 from master
  • Loading branch information
adamchainz authored and felixxm committed Mar 10, 2020
1 parent 0f524f5 commit 6b41f07
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 4 deletions.
86 changes: 86 additions & 0 deletions docs/howto/writing-migrations.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
======================================

Expand Down
20 changes: 16 additions & 4 deletions docs/ref/migration-operations.txt
Original file line number Diff line number Diff line change
Expand Up @@ -423,12 +423,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:

Expand Down

0 comments on commit 6b41f07

Please sign in to comment.