Написание миграций базы данных
В данном документе объясняется, как структурировать и писать миграции базы данных для различных возможных сценариев. Для ознакомительного материала о миграциях см. руководство по темам.
Миграции данных и несколько баз данных
При использовании нескольких баз данных вам может потребоваться определить, нужно ли применять миграцию к конкретной базе данных. Например, вы можете захотеть применить миграцию только к определенной базе данных.
Для этого вы можете проверить псевдоним соединения с базой данных внутри RunPython операции, посмотрев на атрибут schema_editor.connection.alias.
from django.db import migrations
def forwards(apps, schema_editor):
if schema_editor.connection.alias != 'default':
return
# Your migration code goes here
class Migration(migrations.Migration):
dependencies = [
# Dependencies to other migrations
]
operations = [
migrations.RunPython(forwards),
]
Вы также можете предоставить подсказки, которые будут переданы методу allow_migrate() маршрутизаторов базы данных в качестве **hints.
class MyRouter:
def allow_migrate(self, db, app_label, model_name=None, **hints):
if 'target_db' in hints:
return db == hints['target_db']
return True
Затем, чтобы использовать это в своих миграциях, выполните следующие действия:
from django.db import migrations
def forwards(apps, schema_editor):
# Your migration code goes here
...
class Migration(migrations.Migration):
dependencies = [
# Dependencies to other migrations
]
operations = [
migrations.RunPython(forwards, hints={'target_db': 'default'}),
]
Если ваша RunPython или RunSQL операция затрагивает только одну модель, рекомендуется передавать model_name в качестве подсказки, чтобы сделать ее максимально прозрачной для маршрутизатора. Это особенно важно для многоразовых и сторонних приложений.
Миграции, добавляющие уникальные поля
Применение обычной миграции, добавляющей уникальное поле типа «не NULL» в таблицу с существующими строками, вызовет ошибку, поскольку значение, используемое для заполнения существующих строк, генерируется только один раз, тем самым нарушая уникальность.
Поэтому следует выполнить следующие шаги. В этом примере мы добавим поле типа UUIDField «не NULL» со значением по умолчанию. Измените соответствующее поле в соответствии с вашими потребностями.
- Добавьте поле в вашу модель с аргументами
default=uuid.uuid4иunique=True(выберите подходящее значение по умолчанию для типа поля, которое вы добавляете). - Запустите команду
makemigrations. Это должно сгенерировать миграцию сAddFieldоперацией. - Сгенерируйте два пустых файла миграции для того же приложения, дважды выполнив команду
makemigrations myapp --empty. В примерах ниже мы переименовали файлы миграции, чтобы придать им осмысленные имена. -
Скопируйте операцию
AddFieldиз сгенерированной миграции (первый из трех новых файлов) в последнюю миграцию, изменитеAddFieldнаAlterField, и добавьте импортыuuidиmodels. Например:0006_remove_uuid_null.py# Generated by Django A.B on YYYY-MM-DD HH:MM from django.db import migrations, models import uuid class Migration(migrations.Migration): dependencies = [ ('myapp', '0005_populate_uuid_values'), ] operations = [ migrations.AlterField( model_name='mymodel', name='uuid', field=models.UUIDField(default=uuid.uuid4, unique=True), ), ] -
Измените первый файл миграции. Сгенерированный класс миграции должен выглядеть примерно так:
0004_add_uuid_field.pyclass Migration(migrations.Migration): dependencies = [ ('myapp', '0003_auto_20150129_1705'), ] operations = [ migrations.AddField( model_name='mymodel', name='uuid', field=models.UUIDField(default=uuid.uuid4, unique=True), ), ]Измените
unique=Trueнаnull=True— это создаст промежуточное поле null и отложит создание уникального ограничения до тех пор, пока мы не заполним уникальные значения во всех строках. -
В первом пустом файле миграции добавьте операцию
RunPythonилиRunSQLдля генерации уникального значения (UUID в примере) для каждой существующей строки. Также добавьте импортuuid. Например:0005_populate_uuid_values.py# Generated by Django A.B on YYYY-MM-DD HH:MM from django.db import migrations import uuid def gen_uuid(apps, schema_editor): MyModel = apps.get_model('myapp', 'MyModel') for row in MyModel.objects.all(): row.uuid = uuid.uuid4() row.save(update_fields=['uuid']) class Migration(migrations.Migration): dependencies = [ ('myapp', '0004_add_uuid_field'), ] operations = [ # omit reverse_code=... if you don't want the migration to be reversible. migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop), ] -
Теперь вы можете применить миграции как обычно, используя команду
migrate.Обратите внимание, что существует проблема гонки, если вы разрешаете создание объектов во время выполнения этой миграции. Объекты, созданные после
AddFieldи доRunPython, будут иметь свои исходныеuuidперезаписанные.
Неатомарные миграции
В базах данных, поддерживающих транзакции DDL (SQLite и PostgreSQL), миграции по умолчанию выполняются внутри транзакции. Для случаев использования, таких как выполнение миграций данных для больших таблиц, вы можете предотвратить выполнение миграции в транзакции, установив атрибут atomic в значение False.
from django.db import migrations
class Migration(migrations.Migration):
atomic = False
В такой миграции все операции выполняются без транзакции. Возможно выполнить части миграции внутри транзакции, используя atomic() или передавая atomic=True в RunPython.
Вот пример неатомарной миграции данных, обновляющей большую таблицу небольшими партиями:
import uuid
from django.db import migrations, transaction
def gen_uuid(apps, schema_editor):
MyModel = apps.get_model('myapp', 'MyModel')
while MyModel.objects.filter(uuid__isnull=True).exists():
with transaction.atomic():
for row in MyModel.objects.filter(uuid__isnull=True)[:1000]:
row.uuid = uuid.uuid4()
row.save()
class Migration(migrations.Migration):
atomic = False
operations = [
migrations.RunPython(gen_uuid),
]
Атрибут atomic не оказывает влияния на базы данных, которые не поддерживают транзакции DDL (например, MySQL, Oracle). (Поддержка атомарных DDL-операций MySQL относится к отдельным операциям, а не к нескольким операциям, заключенным в транзакцию, которая может быть отменена).
Управление порядком миграций
Django определяет порядок применения миграций не по имени файла каждой миграции, а по построению графа, используя два свойства класса Migration: dependencies и run_before.
Если вы использовали команду makemigrations, вы, вероятно, уже видели dependencies в действии, так как автоматически созданные миграции имеют это определенное свойство как часть процесса создания.
Свойство dependencies объявляется следующим образом:
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('myapp', '0123_the_previous_migration'),
]
Обычно этого достаточно, но время от времени вам может потребоваться убедиться, что ваша миграция выполняется *до* других миграций. Это полезно, например, чтобы миграции сторонних приложений выполнялись *после* замены вашей AUTH_USER_MODEL.
Для этого поместите все миграции, которые должны зависеть от вашей, в атрибут run_before в вашем классе Migration:
class Migration(migrations.Migration):
...
run_before = [
('third_party_app', '0001_do_awesome'),
]
Предпочтительнее использовать dependencies вместо run_before когда это возможно. Вы должны использовать run_before только в том случае, если неуказание dependencies в миграции, которую вы хотите выполнить после той, которую вы пишете, нежелательно или непрактично.
Перенос данных между приложениями сторонних разработчиков
Вы можете использовать миграцию данных для перемещения данных из одного стороннего приложения в другое.
Если вы планируете удалить старое приложение впоследствии, вам необходимо установить свойство dependencies на основе того, установлено ли старое приложение. В противном случае у вас будут отсутствующие зависимости после удаления старого приложения. Аналогично, вам необходимо перехватывать LookupError в вызове apps.get_model(), который извлекает модели из старого приложения. Этот подход позволяет развертывать ваш проект в любом месте, не устанавливая и не удаляя старое приложение предварительно.
Вот пример миграции:
from django.apps import apps as global_apps
from django.db import migrations
def forwards(apps, schema_editor):
try:
OldModel = apps.get_model('old_app', 'OldModel')
except LookupError:
# The old app isn't installed.
return
NewModel = apps.get_model('new_app', 'NewModel')
NewModel.objects.bulk_create(
NewModel(new_attribute=old_object.old_attribute)
for old_object in OldModel.objects.all()
)
class Migration(migrations.Migration):
operations = [
migrations.RunPython(forwards, migrations.RunPython.noop),
]
dependencies = [
('myapp', '0123_the_previous_migration'),
('new_app', '0001_initial'),
]
if global_apps.is_installed('old_app'):
dependencies.append(('old_app', '0001_initial'))
Также подумайте, что должно произойти при отмене миграции. Вы можете либо ничего не делать (как в примере выше), либо удалить часть или все данные из нового приложения. Соответственно скорректируйте второй аргумент операции RunPython.
Изменение ManyToManyField для использования модели through
Если вы изменяете ManyToManyField для использования модели through, стандартная миграция удалит существующую таблицу и создаст новую, потеряв существующие отношения. Чтобы избежать этого, вы можете использовать SeparateDatabaseAndState для переименования существующей таблицы в новое имя таблицы, одновременно сообщая детектору миграций, что новая модель была создана. Вы можете проверить существующее имя таблицы через sqlmigrate или dbshell. Вы можете проверить новое имя таблицы с помощью свойства _meta.db_table модели через модель. Ваша новая модель through должна использовать те же имена для ForeignKeyов, что и Django. Также, если ей нужны дополнительные поля, они должны быть добавлены в операции после SeparateDatabaseAndState.
Например, если у нас была модель Book с ManyToManyField связью с Author, мы могли бы добавить модель через AuthorBook с новым полем is_primary, как показано ниже:
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),
),
]
Изменение необработанной модели на управляемую
Если вы хотите изменить неуправляемую модель (managed=False) на управляемую, вы должны удалить managed=False и сгенерировать миграцию перед внесением других изменений в схему модели, так как изменения схемы, которые появляются в миграции, содержащей операцию по изменению Meta.managed могут не быть применены.
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/3.2/howto/writing-migrations/