Научно-исследовательская работа

перевод статьи

«Копаем глубже в миграции Django»

Содержание

Как Django узнает, какие миграции следует применить

Давайте вспомним самый последний шаг из предыдущей статьи цикла. Вы создали миграцию, а затем применили все доступные миграции с помощью команды python manage.py migrate. Если эта команда была выполнена успешно, то таблицы вашей базы данных теперь соответствуют определениям вашей модели.

Что произойдет, если вы запустите эту команду снова? Давайте попробуем это сделать:

$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, historical_data, sessions
Running migrations:
  No migrations to apply.

 

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

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

Например, вот как выглядит эта таблица в нашем проекте bitcoin_tracker:

ID App Name Applied
1 contenttypes 0001_initial 2019-02-05 20:23:21.461496
2 auth 0001_initial 2019-02-05 20:23:21.489948
3 admin 0001_initial 2019-02-05 20:23:21.508742
4 admin 0002_logentry_remove... 2019-02-05 20:23:21.531390
5 admin 0003_logentry_add_ac... 2019-02-05 20:23:21.564834
6 contenttypes 0002_remove_content_... 2019-02-05 20:23:21.597186
7 auth 0002_alter_permissio... 2019-02-05 20:23:21.608705
8 auth 0003_alter_user_emai... 2019-02-05 20:23:21.628441
9 auth 0004_alter_user_user... 2019-02-05 20:23:21.646824
10 auth 0005_alter_user_last... 2019-02-05 20:23:21.661182
11 auth 0006_require_content... 2019-02-05 20:23:21.663664
12 auth 0007_alter_validator... 2019-02-05 20:23:21.679482
13 auth 0008_alter_user_user... 2019-02-05 20:23:21.699201
14 auth 0009_alter_user_last... 2019-02-05 20:23:21.718652
15 historical_data 0001_initial 2019-02-05 20:23:21.726000
16 sessions 0001_initial 2019-02-05 20:23:21.734611
19 historical_data 0002_switch_to_decimals 2019-02-05 20:30:11.337894

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

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

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

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

Что происходит, когда вы запускаете python manage.py makemigrations? Django ищет изменения, внесенные в модели в вашем приложении . Если он находит какие-либо изменения, например, модель была добавлена, то он создает файл миграции в подкаталоге migrations. Этот файл миграции содержит список операций для приведения схемы вашей базы данных в соответствие с определением модели.

Примечание: Ваше приложение должно быть указано в настройках INSTALLED_APPS, и оно должно содержать каталог migrations с файлом __init__.py. В противном случае Django не будет создавать для него миграции.

Каталог migrations автоматически создается при создании нового приложения с помощью команды управления startapp, но его легко забыть при создании приложения вручную.

Файлы миграции - это просто Python, поэтому давайте посмотрим на первый файл миграции в приложении historical_prices. Вы можете найти его по адресу historical_prices/migrations/0001_initial.py. Он должен выглядеть примерно так:

from django.db import models, migrations

class Migration(migrations.Migration):
    dependencies = []
    operations = [
        migrations.CreateModel(
            name='PriceHistory',
            fields=[
                ('id', models.AutoField(
                    verbose_name='ID',
                    serialize=False,
                    primary_key=True,
                    auto_created=True)),
                ('date', models.DateTimeField(auto_now_add=True)),
                ('price', models.DecimalField(decimal_places=2, max_digits=5)),
                ('volume', models.PositiveIntegerField()),
                ('total_btc', models.PositiveIntegerField()),
            ],
            options={
            },
            bases=(models.Model,),
        ),
    ]

 

Как вы можете видеть, он содержит единственный класс Migration, который наследуется от django.db.migrations.Migration. Это класс, который фреймворк миграции будет искать и выполнять, когда вы попросите его применить миграции.

Класс Migration содержит два основных списка:

зависимости
операции

Операции миграции

Сначала рассмотрим список операций. Эта таблица содержит операции, которые должны быть выполнены в рамках миграции. Операции являются подклассами класса django.db.migrations.operations.base.Operation. Вот общие операции, которые встроены в Django:

Operation Class Description
CreateModel Creates a new model and the corresponding database table
DeleteModel Deletes a model and drops its database table
RenameModel Renames a model and renames its database table
AlterModelTable Renames the database table for a model
AlterUniqueTogether Changes the unique constraints of a model
AlterIndexTogether Changes the indexes of a model
AlterOrderWithRespectTo Creates or deletes the _order column for a model
AlterModelOptions Changes various model options without affecting the database
AlterModelManagers Changes the managers available during migrations
AddField Adds a field to a model and the corresponding column in the database
RemoveField Removes a field from a model and drops the corresponding column from the database
AlterField Changes a field’s definition and alters its database column if necessary
RenameField Renames a field and, if necessary, also its database column
AddIndex Creates an index in the database table for the model
RemoveIndex Removes an index from the database table for the model

Обратите внимание, что операции названы в честь изменений, внесенных в определения моделей, а не действий, выполняемых над базой данных. Когда вы применяете миграцию, каждая операция отвечает за генерацию необходимых SQL-запросов для вашей конкретной базы данных. Например, CreateModel генерирует SQL-запрос CREATE TABLE.

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

Примечание: В некоторых случаях Django может неправильно определить ваши изменения. Если вы переименуете модель и измените несколько ее полей, то Django может принять это за новую модель.

Вместо операции RenameModel и нескольких операций AlterField, он создаст операцию DeleteModel и CreateModel. Вместо того, чтобы переименовать таблицу базы данных для модели, он удалит ее и создаст новую таблицу с новым именем, фактически удалив все ваши данные!

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

Django предоставляет еще три класса операций для расширенных вариантов использования:

RunSQL позволяет запускать пользовательский SQL в базе данных.
RunPython позволяет запускать любой Python-код.
SeparateDatabaseAndState - это специализированная операция для расширенного использования.

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

Начиная с Django 2.0, в django.contrib.postgres.operations появилась пара специфических для PostgreSQL операций, которые можно использовать для установки различных расширений PostgreSQL:

BtreeGinExtension
BtreeGistExtension
CITextExtension
CryptoExtension
HStoreExtension
TrigramExtension
UnaccentExtension

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

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

Зависимости миграции

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

В миграции 0001_initial.py, которую вы видели выше, ничего не нужно применять до нее, поэтому зависимостей нет. Давайте посмотрим на вторую миграцию в приложении historical_prices. В файле 0002_switch_to_decimals.py атрибут dependencies в Migration имеет запись:

from django.db import migrations, models

class Migration(migrations.Migration):
    dependencies = [
        ('historical_data', '0001_initial'),
    ]
    operations = [
        migrations.AlterField(
            model_name='pricehistory',
            name='volume',
            field=models.DecimalField(decimal_places=3, max_digits=7),
        ),
    ]

 

В приведенной выше зависимости говорится, что сначала должна быть запущена миграция 0001_initial приложения historical_data. Это имеет смысл, поскольку миграция 0001_initial создает таблицу, содержащую поле, которое хочет изменить миграция 0002_switch_to_decimals.

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

class Migration(migrations.Migration):
    ...

    dependencies = [
        ('auth', '0009_alter_user_last_name_max_length'),
    ]

 

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

В качестве альтернативы можно также обеспечить выполнение миграции перед другой миграцией с помощью атрибута run_before:

class Migration(migrations.Migration):
    ...

    run_before = [
        ('third_party_app', '0001_initial'),
    ]

 

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

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

Просмотр миграции

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

$ python manage.py sqlmigrate historical_data 0001
BEGIN;
--
-- Create model PriceHistory
--
CREATE TABLE "historical_data_pricehistory" (
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
    "date" datetime NOT NULL,
    "price" decimal NOT NULL,
    "volume" integer unsigned NOT NULL
);
COMMIT;

 

При этом будут перечислены базовые SQL-запросы, которые будут сгенерированы указанной миграцией на основе базы данных в вашем файле settings.py. Когда вы передаете параметр --backwards, Django генерирует SQL для неприменения миграции:

$ python manage.py sqlmigrate --backwards historical_data 0001
BEGIN;
--
-- Create model PriceHistory
--
DROP TABLE "historical_data_pricehistory";
COMMIT;

 

Когда вы увидите результат sqlmigrate для немного более сложной миграции, вы сможете оценить, что вам не нужно создавать весь этот SQL вручную!

Как Django обнаруживает изменения в ваших моделях

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

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

Игра в шахматы с Django

Вы можете рассматривать свои модели как шахматную доску, а Django - как шахматного гроссмейстера, наблюдающего за вашей игрой против себя. Но гроссмейстер не следит за каждым вашим ходом. Гроссмейстер смотрит на доску только тогда, когда вы кричите makemigrations.

Поскольку существует лишь ограниченный набор возможных ходов (а гроссмейстер - гроссмейстер), она может придумать ходы, которые произошли с тех пор, как она в последний раз смотрела на доску. Она делает некоторые заметки и разрешает вам играть до тех пор, пока вы снова не выкрикнете makemigrations.

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

Теперь, когда вы кричите "Миграция", гроссмейстер воспроизводит все записанные ходы на другой шахматной доске и отмечает в электронной таблице, какие из ее записей уже были применены. Эта вторая шахматная доска - ваша база данных, а электронная таблица - таблица django_migrations.

Эта аналогия вполне уместна, потому что она хорошо иллюстрирует некоторые особенности поведения миграций Django:

Миграции Django стараются быть эффективными: Подобно тому, как гроссмейстер предполагает, что вы сделали наименьшее количество ходов, Django будет стараться создавать наиболее эффективные миграции. Если вы добавите в модель поле с именем A, затем переименуете его в B, а затем запустите makemigrations, то Django создаст новую миграцию для добавления поля с именем B.

Миграции Django имеют свои ограничения: Если вы сделаете много ходов, прежде чем позволите гроссмейстеру взглянуть на шахматную доску, то она может оказаться не в состоянии проследить точное движение каждой фигуры. Точно так же Django может не прийти к правильной миграции, если вы сделаете слишком много изменений одновременно.

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

Понимание SeparateDatabaseAndState

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

SeparateDatabaseAndState инстанцируется с двумя списками операций:

state_operations содержит операции, которые применяются только к состоянию проекта.
Операции_базы данных содержат операции, которые применяются только к базе данных.

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

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

Заключение

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

Вы узнали, что:

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

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

В этой статье использовался проект bitcoin_tracker Django, созданный в Django Migrations: A Primer.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *