Spec-Zone.ru › Django 5.2

Миграции

Миграции — это способ Django распространять изменения, которые вы вносите в свои модели (добавление поля, удаление модели и т. д.) в схему вашей базы данных. Они предназначены для большей части автоматизации, но вам нужно знать, когда создавать миграции, когда их запускать и какие распространённые проблемы могут возникнуть.

Команды

Существует несколько команд, которые вы будете использовать для взаимодействия с миграциями и обработкой схемы базы данных в Django:

  • migrate, которая отвечает за применение и отмену миграций.
  • makemigrations, которая отвечает за создание новых миграций на основе изменений, которые вы внесли в свои модели.
  • sqlmigrate, которая отображает SQL-запросы для миграции.
  • showmigrations, которая отображает миграции проекта и их статус.

Вы должны рассматривать миграции как систему контроля версий для схемы вашей базы данных. makemigrations отвечает за упаковку изменений в модели в отдельные файлы миграций — аналогично коммитам, а migrate отвечает за их применение к вашей базе данных.

Файлы миграций для каждой приложения находятся в директории «migrations» внутри этого приложения и предназначены для включения в базу кода и распространения. Вы должны создавать их один раз на своём компьютере разработчика, а затем запускать те же миграции на машинах коллег, на тестовых серверах и, в конечном итоге, на серверах производства.

Примечание

Можно переопределить имя пакета, содержащего миграции, на уровне каждого приложения, изменив настройку MIGRATION_MODULES.

Миграции будут выполняться одинаково на одном наборе данных и дадут согласованные результаты, что означает, что то, что вы видите в разработке и на тестовом сервере, при одинаковых обстоятельствах будет точно таким же на сервере производства.

Django будет создавать миграции для любых изменений в ваших моделях или полях, даже для опций, которые не влияют на базу данных, так как единственный способ правильно восстановить поле — это иметь все изменения в истории, и вам могут понадобиться эти опции в некоторых миграциях данных позже (например, если вы задали пользовательские валидаторы).

Поддержка бэкендов

Миграции поддерживаются всеми бэкендами, которые поставляются с Django, а также всеми сторонними бэкендами, если они запрограммировали поддержку изменения схемы (сделано через класс SchemaEditor).

Однако некоторые базы данных более способны к миграциям схемы, чем другие; некоторые замечания описаны ниже.

PostgreSQL

PostgreSQL — самая способная из всех баз данных здесь в плане поддержки схемы.

MySQL

MySQL не поддерживает транзакции при операциях изменения схемы, что означает, что если миграция не удаётся, вам нужно будет вручную отменить изменения, чтобы попробовать снова (откатиться к более ранней точке невозможно).

MySQL 8.0 представил значительные улучшения производительности для операций DDL, сделав их более эффективными и уменьшив необходимость полного перестроения таблиц. Однако он не может гарантировать полное отсутствие блокировок или прерываний. В ситуациях, когда блокировки всё ещё необходимы, продолжительность этих операций будет пропорциональна количеству строк.

Наконец, MySQL имеет относительно небольшой предел на общий размер всех столбцов, которые охватывает индекс. Это означает, что индексы, которые возможны на других бэкендах, не смогут быть созданы в MySQL.

SQLite

SQLite имеет очень ограниченную встроенную поддержку изменения схемы, и поэтому Django пытается смоделировать её следующим образом:

  • Создание новой таблицы с новой схемой
  • Копирование данных
  • Удаление старой таблицы
  • Переименование новой таблицы для соответствия исходному имени

Этот процесс, как правило, работает хорошо, но может быть медленным и иногда глючить. Не рекомендуется запускать и мигрировать SQLite в рабочей среде, если вы не очень хорошо знакомы с рисками и ограничениями; поддержка, которая поставляется с Django, предназначена для того, чтобы разработчики могли использовать SQLite на своих локальных машинах для разработки менее сложных проектов Django без необходимости полной базы данных.

Процесс

Django может создавать миграции за вас. Внесите изменения в свои модели (скажем, добавьте поле и удалите модель), а затем запустите makemigrations:

$ python manage.py makemigrations
Migrations for 'books':
  books/migrations/0003_auto.py:
    ~ Alter field author on book

Ваши модели будут сканироваться и сравниваться с версиями, которые в настоящее время содержатся в ваших файлах миграций, а затем будет создан новый набор миграций. Убедитесь, что вы прочитали вывод, чтобы увидеть, что makemigrations считает, что вы изменили — это не идеально, и для сложных изменений оно может не обнаруживать ожидаемого.

После получения новых файлов миграций вы должны применить их к своей базе данных, чтобы убедиться, что они работают как ожидается:

$ python manage.py migrate
Operations to perform:
  Apply all migrations: books
Running migrations:
  Rendering model states... DONE
  Applying books.0003_auto... OK

После применения миграции добавьте миграцию и изменения в модели в систему контроля версий как один коммит — таким образом, когда другие разработчики (или ваши серверы производства) получат код, они получат и изменения в ваших моделях, и соответствующую миграцию одновременно.

Если вы хотите присвоить миграции(ям) осмысленное имя вместо сгенерированного, вы можете использовать параметр makemigrations --name:

$ python manage.py makemigrations --name changed_my_model your_app_label

Система контроля версий

Поскольку миграции хранятся в системе контроля версий, вы иногда можете столкнуться с ситуацией, когда вы и другой разработчик оба закоммитили миграцию в одно и то же приложение в одно и то же время, в результате чего две миграции имеют одинаковый номер.

Не волнуйтесь — номера предназначены только для справки разработчиков, Django заботится только о том, чтобы каждая миграция имела другое имя. Миграции указывают, от каких других миграций они зависят — включая более ранние миграции в том же приложении — в файле, поэтому можно обнаружить, когда для одного приложения есть две новые миграции, которые не упорядочены.

В этом случае Django запросит у вас некоторые варианты. Если он сочтёт это достаточно безопасным, он предложит автоматически упорядочить две миграции за вас. В противном случае вам нужно будет внести изменения в миграции самостоятельно — не волнуйтесь, это не сложно и подробно описано в разделе Файлы миграций ниже.

Транзакции

В базах данных, поддерживающих транзакции DDL (SQLite и PostgreSQL), все операции миграции по умолчанию выполняются в одной транзакции. В отличие от этого, если база данных не поддерживает транзакции DDL (например, MySQL, Oracle), все операции выполняются без транзакции.

Вы можете предотвратить выполнение миграции в транзакции, установив атрибут atomic в значение False. Например:

from django.db import migrations


class Migration(migrations.Migration):
    atomic = False

Также можно выполнить части миграции внутри транзакции, используя atomic() или передав atomic=True в RunPython. Дополнительные сведения см. в разделе Неатомарные миграции.

Зависимости

Хотя миграции относятся к каждому приложению, таблицы и взаимосвязи, подразумеваемые вашими моделями, слишком сложны для создания по одному приложению за раз. Когда вы создаёте миграцию, которая требует выполнения чего-то ещё, например, вы добавляете ForeignKey в приложении books в приложение authors, результирующая миграция будет содержать зависимость от миграции в приложении authors.

Это означает, что при выполнении миграций миграция authors выполняется первой и создаёт таблицу, на которую ссылается ForeignKey, а затем миграция, создающая столбец ForeignKey, выполняется позже и создаёт ограничение. Если этого не произойдёт, миграция попытается создать столбец ForeignKey без существующей таблицы, на которую он ссылается, и ваша база данных выдаст ошибку.

Это поведение зависимостей влияет на большинство операций миграций, где вы ограничиваетесь одним приложением. Ограничение одного приложения (либо в makemigrations, либо в migrate) — это обещание, основанное на лучших усилиях, а не гарантия; любые другие приложения, необходимые для правильной обработки зависимостей, будут использоваться.

Приложения без миграций не должны иметь отношений (ForeignKey, ManyToManyField и т. д.) с приложениями с миграциями. Иногда это может работать, но это не поддерживается.

Заменяемые зависимости

django.db.migrations.swappable_dependency(value)

Функция swappable_dependency() используется в миграциях для объявления «заменяемых» зависимостей от миграций в приложении, содержащем модель с заменой, в настоящее время — от первой миграции этого приложения. Соответственно, модель с заменой должна быть создана в начальной миграции. Аргумент value — строка "<app label>.<model>", описывающая метку приложения и имя модели, например "myapp.MyModel".

Используя swappable_dependency(), вы сообщаете фреймворку миграции, что миграция зависит от другой миграции, которая настраивает заменяемую модель, позволяя в будущем заменить модель другой реализацией. Это обычно используется для ссылки на модели, которые могут быть настроены или заменены, такие как пользовательская модель (settings.AUTH_USER_MODEL, которая по умолчанию является "auth.User") в системе аутентификации Django.

Файлы миграций

Миграции хранятся в формате на диске, здесь называемом «файлами миграций». Эти файлы — обычные Python-файлы с согласованной структурой объектов, написанные в декларативном стиле.

Файл миграции базового уровня выглядит так:

from django.db import migrations, models


class Migration(migrations.Migration):
    dependencies = [("migrations", "0001_initial")]

    operations = [
        migrations.DeleteModel("Tribble"),
        migrations.AddField("Author", "rating", models.IntegerField(default=0)),
    ]

Django при загрузке файла миграции (как Python-модуля) ищет подкласс класса django.db.migrations.Migration, называемый Migration. Затем он проверяет этот объект на наличие четырех атрибутов, из которых два используются чаще всего:

  • dependencies, список миграций, от которых зависит данная миграция.
  • operations, список классов Operation, определяющих действия этой миграции.

Операции — ключевой момент; они представляют собой набор декларативных инструкций, которые сообщают Django о необходимых изменениях схемы. Django сканирует их и создает представление во внутренней памяти всех изменений схемы для всех приложений, а затем использует его для генерации SQL-кода, осуществляющего эти изменения схемы.

Эта структура в памяти также используется для определения различий между вашими моделями и текущим состоянием ваших миграций; Django выполняет все изменения по порядку над набором моделей в памяти, чтобы получить состояние ваших моделей в последний раз, когда вы запускали makemigrations. Затем он использует эти модели для сравнения с моделями в ваших файлах models.py, чтобы определить, что вы изменили.

Вам вряд ли понадобится вручную редактировать файлы миграций, но это вполне возможно, если нужно. Некоторые более сложные операции не могут быть автоматически обнаружены и доступны только через написанные вручную миграции, так что не бойтесь их редактировать, если это необходимо.

Пользовательские поля

Вы не можете изменить количество позиционных аргументов в уже мигрированном пользовательском поле, не вызвав TypeError. Старая миграция вызовет измененный метод __init__ со старой сигнатурой. Поэтому, если вам нужен новый аргумент, создайте ключевой аргумент и добавьте что-то вроде assert 'argument_name' in kwargs в конструктор.

Менеджеры моделей

Вы можете по желанию сериализовать менеджеры в миграции и использовать их в операциях RunPython. Это делается путем определения атрибута use_in_migrations в классе менеджера:

class MyManager(models.Manager):
    use_in_migrations = True


class MyModel(models.Model):
    objects = MyManager()

Если вы используете функцию from_queryset() для динамической генерации класса менеджера, вам нужно унаследовать от сгенерированного класса, чтобы сделать его импортируемым:

class MyManager(MyBaseManager.from_queryset(CustomQuerySet)):
    use_in_migrations = True


class MyModel(models.Model):
    objects = MyManager()

Обратите внимание на замечания о Исторических моделях в миграциях, чтобы узнать о связанных последствиях.

Первоначальные миграции

Migration.initial

«Первоначальные миграции» для приложения — это миграции, создающие первую версию таблиц этого приложения. Обычно приложение имеет одну первоначальную миграцию, но в некоторых случаях сложных взаимозависимостей моделей их может быть две или более.

Первоначальные миграции помечаются атрибутом класса initial = True в классе миграции. Если атрибут класса initial не найден, миграция считается «первоначальной», если это первая миграция в приложении (то есть если она не зависит ни от какой другой миграции в том же приложении).

При использовании опции migrate --fake-initial эти первоначальные миграции обрабатываются особым образом. Для первоначальной миграции, создающей одну или несколько таблиц (операция CreateModel), Django проверяет, существуют ли все эти таблицы в базе данных, и подменяет применение миграции, если это так. Аналогично, для первоначальной миграции, добавляющей одно или несколько полей (операция AddField), Django проверяет, существуют ли все соответствующие столбцы в базе данных, и подменяет применение миграции, если это так. Без --fake-initial первоначальные миграции обрабатываются так же, как и любые другие миграции.

Согласованность истории миграций

Как уже обсуждалось, при объединении двух ветвей разработки может потребоваться вручную линейно упорядочить миграции. При редактировании зависимостей миграций вы можете случайно создать несогласованное состояние истории, где миграция применена, но некоторые из ее зависимостей нет. Это явный признак того, что зависимости неправильные, поэтому Django откажется от выполнения миграций или создания новых, пока это не будет исправлено. При использовании нескольких баз данных вы можете использовать метод allow_migrate() маршрутизаторов баз данных, чтобы контролировать, какие базы данных makemigrations проверяет на согласованность истории.

Добавление миграций в приложения

Новые приложения предварительно настроены для приема миграций, поэтому вы можете добавлять миграции, выполнив makemigrations после внесения изменений.

Если ваше приложение уже имеет модели и таблицы базы данных, но еще не имеет миграций (например, вы создали его на основе предыдущей версии Django), вам нужно будет преобразовать его для использования миграций, выполнив:

$ python manage.py makemigrations your_app_label

Это создаст новую первоначальную миграцию для вашего приложения. Теперь выполните python manage.py migrate --fake-initial, и Django обнаружит, что у вас есть первоначальная миграция и что таблицы, которые он хочет создать, уже существуют, и пометит миграцию как уже примененную. (Без флага migrate --fake-initial команда выдаст ошибку, поскольку таблицы, которые он хочет создать, уже существуют.)

Обратите внимание, что это работает только при соблюдении двух условий:

  • Вы не изменяли свои модели с момента создания их таблиц. Для работы миграций вы должны сначала создать первоначальную миграцию, а затем внести изменения, поскольку Django сравнивает изменения с файлами миграций, а не с базой данных.
  • Вы не изменяли базу данных вручную — Django не сможет определить, что ваша база данных не соответствует вашим моделям, вы просто получите ошибки, когда миграции попытаются изменить эти таблицы.

Отмена миграций

Миграции можно отменить с помощью migrate, передав номер предыдущей миграции. Например, для отмены миграции books.0003:

$ python manage.py migrate books 0002
Operations to perform:
  Target specific migration: 0002_auto, from books
Running migrations:
  Rendering model states... DONE
  Unapplying books.0003_auto... OK
...\> py manage.py migrate books 0002
Operations to perform:
  Target specific migration: 0002_auto, from books
Running migrations:
  Rendering model states... DONE
  Unapplying books.0003_auto... OK

Если вы хотите отменить все примененные миграции для приложения, используйте имя zero:

$ python manage.py migrate books zero
Operations to perform:
  Unapply all migrations: books
Running migrations:
  Rendering model states... DONE
  Unapplying books.0002_auto... OK
  Unapplying books.0001_initial... OK
...\> py manage.py migrate books zero
Operations to perform:
  Unapply all migrations: books
Running migrations:
  Rendering model states... DONE
  Unapplying books.0002_auto... OK
  Unapplying books.0001_initial... OK

Миграция необратима, если она содержит какие-либо необратимые операции. Попытка отменить такие миграции вызовет IrreversibleError:

$ python manage.py migrate books 0002
Operations to perform:
  Target specific migration: 0002_auto, from books
Running migrations:
  Rendering model states... DONE
  Unapplying books.0003_auto...Traceback (most recent call last):
django.db.migrations.exceptions.IrreversibleError: Operation <RunSQL  sql='DROP TABLE demo_books'> in books.0003_auto is not reversible
...\> py manage.py migrate books 0002
Operations to perform:
  Target specific migration: 0002_auto, from books
Running migrations:
  Rendering model states... DONE
  Unapplying books.0003_auto...Traceback (most recent call last):
django.db.migrations.exceptions.IrreversibleError: Operation <RunSQL  sql='DROP TABLE demo_books'> in books.0003_auto is not reversible

Исторические модели

При выполнении миграций Django работает с историческими версиями ваших моделей, хранящимися в файлах миграций. Если вы пишете Python-код, использующий операцию RunPython, или если у вас есть методы allow_migrate в маршрутизаторах баз данных, вы обязаны использовать эти исторические версии моделей, а не импортировать их напрямую.

Предупреждение

Если вы импортируете модели напрямую, а не используете исторические модели, ваши миграции могут работать первоначально, но в будущем они потерпят неудачу при повторном запуске старых миграций (обычно при настройке новой установки и выполнении всех миграций для настройки базы данных).

Это означает, что проблемы с историческими моделями могут не быть очевидны сразу. Если у вас возникнет такой сбой, не стесняйтесь отредактировать миграцию, чтобы использовать исторические модели вместо прямых импортов, и внесите эти изменения.

Поскольку невозможно сериализовать произвольный Python-код, эти исторические модели не будут иметь никаких пользовательских методов, которые вы определили. Тем не менее, они будут иметь те же поля, отношения, менеджеры (ограниченные менеджерами с use_in_migrations = True) и параметры Meta (также версионированы, поэтому они могут отличаться от ваших текущих).

Предупреждение

Это означает, что вы не будете вызывать пользовательские save()-методы на объектах при доступе к ним в миграциях, и у вас не будет пользовательских конструкторов или методов экземпляров. Планируйте соответствующим образом!

Ссылки на функции в параметрах полей, такие как upload_to и limit_choices_to, и объявления менеджеров моделей с менеджерами, имеющими use_in_migrations = True, сериализуются в миграции, поэтому функции и классы должны храниться до тех пор, пока существует миграция, ссылающаяся на них. Любые пользовательские поля моделей также должны храниться, так как они импортируются миграциями напрямую.

Кроме того, конкретные базовые классы модели хранятся в виде указателей, поэтому вы всегда должны сохранять базовые классы до тех пор, пока существует миграция, содержащая ссылку на них. С положительной стороны, методы и менеджеры из этих базовых классов наследуются нормально, поэтому если вам абсолютно необходим доступ к ним, вы можете перенести их в суперкласс.

Чтобы удалить старые ссылки, вы можете объединить миграции или, если ссылок не так много, скопировать их в файлы миграций.

Особенности удаления полей модели

Аналогично «условиям, связанным со ссылками на исторические функции», описанным в предыдущем разделе, удаление пользовательских полей модели из вашего проекта или стороннего приложения создаст проблему, если они упоминаются в старых миграциях.

Для решения этой ситуации Django предоставляет некоторые атрибуты полей модели, помогающие в устаревании полей модели, используя рамку проверок системы.

Добавьте атрибут system_check_deprecated_details к полю вашей модели, аналогично следующему:

class IPAddressField(Field):
    system_check_deprecated_details = {
        "msg": (
            "IPAddressField has been deprecated. Support for it (except "
            "in historical migrations) will be removed in Django 1.9."
        ),
        "hint": "Use GenericIPAddressField instead.",  # optional
        "id": "fields.W900",  # pick a unique ID for your field.
    }

После периода устаревания, выбранного вами (два или три выпуска функций для полей в самом Django), измените атрибут system_check_deprecated_details на system_check_removed_details и обновите словарь, аналогично:

class IPAddressField(Field):
    system_check_removed_details = {
        "msg": (
            "IPAddressField has been removed except for support in "
            "historical migrations."
        ),
        "hint": "Use GenericIPAddressField instead.",
        "id": "fields.E900",  # pick a unique ID for your field.
    }

Вы должны сохранить методы поля, необходимые для его работы в миграциях базы данных, такие как __init__(), deconstruct() и get_internal_type(). Поддерживайте это заглушечное поле до тех пор, пока существуют какие-либо миграции, которые ссылаются на это поле. Например, после сжатия миграций и удаления старых вы должны иметь возможность полностью удалить поле.

Миграции данных

Помимо изменения схемы базы данных, вы также можете использовать миграции для изменения данных в самой базе данных, в сочетании со схемой, если это необходимо.

Миграции, изменяющие данные, обычно называются «миграциями данных»; их лучше всего писать как отдельные миграции, наряду с вашими миграциями схемы.

Django не может автоматически генерировать миграции данных для вас, как это делает с миграциями схемы, но это не очень сложно. Файлы миграций в Django состоят из операций, и основная операция, которую вы используете для миграций данных, это RunPython.

Для начала создайте пустой файл миграции, с которым можно работать (Django поместит файл в нужное место, предложит имя и добавит зависимости за вас):

python manage.py makemigrations --empty yourappname

Затем откройте файл; он должен выглядеть примерно так:

# Generated by Django A.B on YYYY-MM-DD HH:MM
from django.db import migrations


class Migration(migrations.Migration):
    dependencies = [
        ("yourappname", "0001_initial"),
    ]

    operations = []

Теперь все, что вам нужно сделать, это создать новую функцию и использовать RunPython. RunPython ожидает вызываемую функцию в качестве аргумента, которая принимает два аргумента — первый — это реестр приложений, в котором загружены исторические версии всех ваших моделей, чтобы соответствовать тому, где в вашей истории находится миграция, а второй — SchemaEditor, с помощью которого можно вручную изменять схему базы данных (но будьте осторожны, так как это может сбить с толку автообнаруживатель миграций!).

Давайте напишем миграцию, которая заполнит наше новое поле name комбинированными значениями first_name и last_name (мы пришли к выводу, что не у всех есть имя и фамилия). Все, что нам нужно сделать, это использовать историческую модель и пройтись по строкам:

from django.db import migrations


def combine_names(apps, schema_editor):
    # We can't import the Person model directly as it may be a newer
    # version than this migration expects. We use the historical version.
    Person = apps.get_model("yourappname", "Person")
    for person in Person.objects.all():
        person.name = f"{person.first_name} {person.last_name}"
        person.save()


class Migration(migrations.Migration):
    dependencies = [
        ("yourappname", "0001_initial"),
    ]

    operations = [
        migrations.RunPython(combine_names),
    ]

После этого мы можем запустить python manage.py migrate как обычно, и миграция данных будет выполняться параллельно с другими миграциями.

Вы можете передать вторую вызываемую функцию в RunPython для выполнения любого логики, которую вы хотите выполнить при обратной миграции. Если эта вызываемая функция опущена, обратная миграция вызовет исключение.

Доступ к моделям из других приложений

При написании функции RunPython, использующей модели из приложений, отличных от того, в котором находится миграция, атрибут dependencies миграции должен включать последнюю миграцию каждого вовлеченного приложения, в противном случае вы можете получить ошибку, подобную: LookupError: No installed app with label 'myappname', когда вы пытаетесь получить модель в функции RunPython, используя apps.get_model().

В приведенном ниже примере у нас есть миграция в app1, которая должна использовать модели в app2. Мы не заинтересованы в деталях move_m1, кроме того факта, что ей потребуется доступ к моделям из обоих приложений. Поэтому мы добавили зависимость, которая указывает последнюю миграцию app2:

class Migration(migrations.Migration):
    dependencies = [
        ("app1", "0001_initial"),
        # added dependency to enable using models from app2 in move_m1
        ("app2", "0004_foobar"),
    ]

    operations = [
        migrations.RunPython(move_m1),
    ]

Более продвинутые миграции

Если вы заинтересованы в более сложных операциях миграции или хотите написать свои, см. справочник по операциям миграции и «как сделать» в создании миграций.

Сжатие миграций

Рекомендуется свободно создавать миграции и не беспокоиться о том, сколько их у вас; код миграций оптимизирован для обработки сотен миграций без существенного замедления. Однако в конечном итоге вы захотите перейти от нескольких сотен миграций к нескольким, и именно здесь пригодится сжатие.

Сжатие — это процесс сокращения набора существующих миграций до одной (или иногда нескольких) миграций, которые по-прежнему представляют те же изменения.

Django делает это, взяв все ваши существующие миграции, извлекая из них Operation и поместив их в последовательность, а затем запустив оптимизатор, чтобы попытаться сократить длину списка — например, он знает, что CreateModel и DeleteModel взаимно компенсируют друг друга, и он знает, что AddField можно объединить с CreateModel.

После того, как последовательность операций была сокращена максимально — объем возможного сокращения зависит от степени взаимосвязи ваших моделей и наличия операций RunSQL или RunPython (которые не могут быть оптимизированы, если не помечены как elidable), Django запишет ее в новый набор файлов миграций.

Эти файлы помечены как заменяющие ранее сжатые миграции, поэтому они могут сосуществовать со старыми файлами миграций, и Django будет интеллектуально переключаться между ними в зависимости от того, на каком этапе истории вы находитесь. Если вы все еще находитесь в процессе выполнения набора миграций, которые вы сжали, он будет продолжать использовать их, пока не достигнет конца, а затем переключится на сжатую историю, в то время как новые установки будут использовать новую сжатую миграцию и пропустят все старые.

Это позволяет вам сжимать, не повреждая системы, которые в настоящее время находятся в производстве и еще не полностью обновлены. Рекомендуемый процесс — это сжатие, сохранение старых файлов, коммитирование и релиз, ожидание, пока все системы будут обновлены с помощью нового релиза (или, если вы являетесь сторонним проектом, убедитесь, что ваши пользователи обновляют релизы в порядке, не пропуская ни одного), а затем удаление старых файлов, коммитирование и второй релиз.

Команда, которая поддерживает все это, — это squashmigrations — передайте ей имя приложения и имя миграции, до которых вы хотите сжать, и она приступит к работе:

$ ./manage.py squashmigrations myapp 0004
Will squash the following migrations:
 - 0001_initial
 - 0002_some_change
 - 0003_another_change
 - 0004_undo_something
Do you wish to proceed? [y/N] y
Optimizing...
  Optimized from 12 operations to 7 operations.
Created new squashed migration /home/andrew/Programs/DjangoTest/test/migrations/0001_squashed_0004_undo_something.py
  You should commit this migration but leave the old ones in place;
  the new migration will be used for new installs. Once you are sure
  all instances of the codebase have applied the migrations you squashed,
  you can delete them.

Используйте опцию squashmigrations --squashed-name, если вы хотите установить имя сжатой миграции вместо использования автоматически сгенерированного.

Обратите внимание, что взаимозависимости моделей в Django могут быть очень сложными, и сжатие может привести к миграциям, которые не будут работать; либо неправильно оптимизированы (в этом случае вы можете попробовать снова с помощью --no-optimize, хотя вам также следует сообщить об ошибке), или с CircularDependencyError, в этом случае вы можете вручную решить ее.

Чтобы вручную устранить CircularDependencyError, выделите одну из ForeignKey в циклической зависимости в отдельную миграцию и перенесите зависимость от другого приложения вместе с ней. Если вы не уверены, посмотрите, как makemigrations справляется с проблемой, когда его просят создать новые миграции из ваших моделей. В будущих версиях Django squashmigrations будет обновлен, чтобы попытаться самостоятельно устранить эти ошибки.

После сжатия миграции вы должны зафиксировать ее вместе с миграциями, которые она заменяет, и распространить это изменение на все работающие экземпляры вашего приложения, убедившись, что они выполняют migrate для сохранения изменения в их базе данных.

Затем вы должны перевести сжатую миграцию в обычную миграцию, выполнив:

  • Удаление всех файлов миграций, которые она заменяет.
  • Обновление всех миграций, которые зависят от удаленных миграций, так, чтобы они зависели от сжатой миграции вместо этого.
  • Удаление атрибута replaces в классе Migration сжатой миграции (так Django определяет, что это сжатая миграция).

Примечание

После сжатия миграции не следует повторно сжимать эту сжатую миграцию, пока вы не переведете ее в обычную миграцию.

Очистка ссылок на удаленные миграции

Если есть вероятность, что вы в будущем будете повторно использовать имя удаленной миграции, вы должны удалить ссылки на нее из таблицы миграций Django с помощью опции migrate --prune.

Сериализация значений

Миграции — это файлы Python, содержащие старые определения ваших моделей; следовательно, для их записи Django должен получить текущее состояние ваших моделей и сериализовать их в файл.

Хотя Django может сериализовать большинство вещей, некоторые вещи мы просто не можем сериализовать в действительное представление Python — нет стандартного Python-метода, как значение можно преобразовать обратно в код (repr() работает только с базовыми значениями и не указывает пути импорта).

Django может сериализовать следующее:

  • int, float, bool, str, bytes, None, NoneType
  • list, set, tuple, dict, range.
  • datetime.date, datetime.time и datetime.datetime экземпляры (включая те, которые учитывают часовой пояс)
  • decimal.Decimal экземпляры
  • enum.Enum и enum.Flag экземпляры
  • uuid.UUID экземпляры
  • functools.partial() и functools.partialmethod экземпляры, имеющие сериализуемые func, args и keywords значения.
  • Чистые и конкретные объекты пути из pathlib. Конкретные пути преобразуются в их эквиваленты чистых путей, например, pathlib.PosixPath в pathlib.PurePosixPath.
  • os.PathLike экземпляры, например, os.DirEntry, которые преобразуются в str или bytes с помощью os.fspath().
  • LazyObject экземпляры, которые оборачивают сериализуемое значение.
  • Типы перечислений (например, TextChoices или IntegerChoices) экземпляры.
  • Любое поле Django
  • Любая ссылка на функцию или метод (например, datetime.datetime.today) (должна находиться в глобальной области видимости модуля)

    • Функции могут быть декорированы, если они корректно обернуты, т. е. с помощью functools.wraps()
    • Декораторы functools.cache() и functools.lru_cache() явно поддерживаются
  • Несвязанные методы, используемые внутри тела класса
  • Любая ссылка на класс (должна находиться в глобальной области видимости модуля)
  • Всё, что имеет пользовательский метод deconstruct() (см. ниже)

Django не может сериализовать:

  • Вложенные классы
  • Произвольные экземпляры классов (например, MyClass(4.3, 5.7))
  • Лямбда-функции

Пользовательские сериализаторы

Вы можете сериализовать другие типы, написав пользовательский сериализатор. Например, если Django по умолчанию не сериализовал Decimal, вы можете сделать это:

from decimal import Decimal

from django.db.migrations.serializer import BaseSerializer
from django.db.migrations.writer import MigrationWriter


class DecimalSerializer(BaseSerializer):
    def serialize(self):
        return repr(self.value), {"from decimal import Decimal"}


MigrationWriter.register_serializer(Decimal, DecimalSerializer)

Первый аргумент MigrationWriter.register_serializer() — это тип или итерируемый набор типов, которые должны использовать сериализатор.

Метод serialize() вашего сериализатора должен возвращать строку, описывающую, как значение должно отображаться в миграциях, и набор любых необходимых импортов в миграцию.

Добавление метода deconstruct()

Вы можете позволить Django сериализовать экземпляры собственного пользовательского класса, предоставив классу метод deconstruct(). Он не принимает аргументов и должен возвращать кортеж из трёх вещей (path, args, kwargs):

  • path должен быть путём к классу в Python, с включённым именем класса в качестве последней части (например, myapp.custom_things.MyClass). Если ваш класс недоступен на верхнем уровне модуля, он не может быть сериализован.
  • args должен быть списком позиционных аргументов для передачи методу __init__ вашего класса. Все элементы в этом списке должны быть сериализуемыми.
  • kwargs должен быть словарем именованных аргументов для передачи методу __init__ вашего класса. Каждое значение должно быть сериализуемым.

Примечание

Это возвращаемое значение отличается от метода deconstruct() для пользовательских полей, который возвращает кортеж из четырёх элементов.

Django запишет значение как экземпляр вашего класса с заданными аргументами, подобно тому, как он записывает ссылки на поля Django.

Чтобы предотвратить создание новой миграции каждый раз, когда выполняется makemigrations, вы также должны добавить метод __eq__() в декорированный класс. Эта функция будет вызываться механизмом миграций Django для обнаружения изменений между состояниями.

Пока все аргументы конструктора вашего класса являются сериализуемыми, вы можете использовать декоратор класса @deconstructible из django.utils.deconstruct для добавления метода deconstruct():

from django.utils.deconstruct import deconstructible


@deconstructible
class MyCustomClass:
    def __init__(self, foo=1):
        self.foo = foo
        ...

    def __eq__(self, other):
        return self.foo == other.foo

Декоратор добавляет логику захвата и сохранения аргументов при их передаче в ваш конструктор, а затем возвращает эти аргументы точно при вызове deconstruct().

Поддержка нескольких версий Django

Если вы являетесь разработчиком стороннего приложения с моделями, вам может потребоваться отправлять миграции, поддерживающие несколько версий Django. В этом случае вы всегда должны запускать makemigrations с самой низкой поддерживаемой версией Django.

Система миграций будет сохранять обратную совместимость в соответствии с той же политикой, что и остальная часть Django, поэтому файлы миграций, сгенерированные в Django X.Y, должны работать без изменений в Django X.Y+1. Однако система миграций не гарантирует совместимость в будущем. Могут быть добавлены новые функции, и файлы миграций, сгенерированные с более новыми версиями Django, могут не работать с более старыми версиями.

См. также

Справочник по операциям миграций

Охватывает API операций со схемой, специальные операции и написание собственных операций.

Руководство по написанию миграций

Объясняет структуру и написание миграций базы данных для различных сценариев, с которыми вы можете столкнуться.

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/5.2/topics/migrations/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API