Миграция базы данных Django: всесторонний обзор

Оглавление

Введение в базы данных Django

Веб-фреймворк Django предназначен для работы с бэкендом реляционной базы данных на основе SQL, чаще всего PostgreSQL или MySQL. Если вы никогда раньше не работали непосредственно с реляционной базой данных, управление хранением/доступом к данным и их согласованность с кодом приложения - важный навык, которым необходимо овладеть.

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

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

Django ORM поставляется с системой управления этими миграциями для упрощения процесса синхронизации кода приложения и схемы базы данных.

Решение для миграции баз данных в Django

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

1. Изменить контракт: ORM от Django

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

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

2. Планируйте изменения: создавайте миграции

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

3. Выполнение: применить миграции

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

Отслеживание изменений с помощью Django

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

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

Откат с помощью Django

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

Пример простой миграции баз данных Django

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

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

Начальное приложение Django

В целях демонстрации мы создадим очень простой проект Django под названием Foo:

django-admin startproject foo

В этом проекте мы установим наше приложение для ведения блога. Из базового каталога проекта: ./manage.py startapp blog

Зарегистрируйте наше новое приложение с нашим проектом в foo/settings.py, добавив `blog` в INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    'blog',
]

В blog/models.py мы можем определить нашу начальную модель данных:

class Post(models.Model):
    slug = models.SlugField(max_length=50, unique=True)
    title = models.CharField(max_length=50)
    body = models.TextField()

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

Теперь, когда у нас определена начальная модель данных, мы можем создать миграции, которые настроят нашу базу данных: ./manage.py makemigrations

Обратите внимание, что вывод этой команды показывает, что новый файл миграции был создан по адресу

blog/migrations/0001_initial.py, содержащий команду на CreateModel name=‘Post’.

Если мы откроем файл миграции, он будет выглядеть примерно так:

# Generated by Django 2.2 on 2019-04-21 18:04

from django.db import migrations, models

class Migration(migrations.Migration):
    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Post',
            fields=[
                ('id', models.AutoField(
                    auto_created=True, 
                    primary_key=True, 
                    serialize=False, 
                    verbose_name='ID'
                )),
                ('slug', models.SlugField(unique=True)),
                ('title', models.CharField(max_length=50)),
                ('body', models.TextField()),
            ],
        ),
    ]

Большую часть содержимого миграции довольно легко понять. Эта начальная миграция была сгенерирована автоматически, не имеет зависимостей и состоит из одной операции: создать Post Model.

Теперь давайте создадим начальную базу данных SQLite с нашей моделью данных:

./manage.py migrate

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

Для входа в инструмент командной строки SQLite3 выполните:

sqlite3 db.sqlite3

В этом инструменте перечислите все таблицы, созданные в результате первоначальной миграции:

sqlite> .tables

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

В инструменте командной строки SQLite3 можно распечатать содержимое таблицы django_migrations:

sqlite> select * from django_migrations;

Здесь будут показаны все миграции, которые были запущены для вашего приложения. Если вы просмотрите список, вы найдете запись, указывающую на то, что 0001_initial migration была запущена для приложения blog. Так Django узнает, что ваша миграция была применена.

Изменение модели данных Django

Теперь, когда начальное приложение настроено, давайте внесем изменения в модель данных. Во-первых, мы добавим поле published_on к нашему Post Model. Это поле будет nullable. Когда мы захотим опубликовать что-то, мы сможем просто указать, когда это было опубликовано.

Наш новый Post Model теперь будет:

from django.db import models

class Post(models.Model):
    slug = models.SlugField(max_length=50, unique=True)
    title = models.CharField(max_length=50)
    body = models.TextField()
    published_on = models.DateTimeField(null=True, blank=True)

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

from django.conf import settings
from django.db import models

class FeedbackOption(models.Model):
    slug = models.SlugField(max_length=50, unique=True)
    option = models.CharField(max_length=50)

class PostFeedback(models.Model):
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL, related_name='feedback',
        on_delete=models.CASCADE
    )
    post = models.ForeignKey(
        'Post', related_name='feedback', on_delete=models.CASCADE
    )
    option = models.ForeignKey(
        'FeedbackOption', related_name='feedback', on_delete=models.CASCADE
    )

Сгенерируйте миграцию базы данных Django

После внесения изменений в модель, давайте сгенерируем наши новые миграции:

./manage.py makemigrations

Обратите внимание, что на этот раз вывод показывает новый файл миграции, blog/migrations/0002_auto_<YYYYMMDD>_<...>.py, со следующими изменениями:

  • Создайте модель FeedbackOption
  • Добавьте поле published_on в Post
  • Создайте модель PostFeedback

Вот три изменения, которые мы внесли в нашу модель данных.

Теперь, если мы продолжим и откроем сгенерированный файл, он будет выглядеть примерно так:

# Generated by Django 2.2 on 2019-04-21 19:31

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion

class Migration(migrations.Migration):

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ('blog', '0001_initial'),
    ]

    operations = [
        migrations.CreateModel(
            name='FeedbackOption',
            fields=[
                ('id', models.AutoField(
                    auto_created=True,
                    primary_key=True,
                    serialize=False, verbose_name='ID'
                )),
                ('slug', models.SlugField(unique=True)),
                ('option', models.CharField(max_length=50)),
            ],
        ),
        migrations.AddField(
            model_name='post',
            name='published_on',
            field=models.DateTimeField(blank=True, null=True),
        ),
        migrations.CreateModel(
            name='PostFeedback',
            fields=[
                ('id', models.AutoField(
                    auto_created=True,
                    primary_key=True,
                    serialize=False,
                    verbose_name='ID'
                )),
                ('option', models.ForeignKey(
                    on_delete=django.db.models.deletion.CASCADE,
                    related_name='feedback',
                    to='blog.FeedbackOption'
                )),
                ('post', models.ForeignKey(
                    on_delete=django.db.models.deletion.CASCADE,
                    related_name='feedback',
                    to='blog.Post'
                )),
                ('user', models.ForeignKey(
                    on_delete=django.db.models.deletion.CASCADE,
                    related_name='feedback',
                    to=settings.AUTH_USER_MODEL
                )),
            ],
        ),
    ]

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

Применение миграции базы данных Django

Теперь, когда мы создали наши миграции, мы можем применить миграции:

./manage.py migrate

Вывод сообщает нам, что применена последняя сгенерированная миграция. Если мы проверим нашу модифицированную базу данных SQLite, то увидим, что наш новый файл миграции должен находиться в таблице django_migrations, новые таблицы должны присутствовать, а наше новое поле в Post Model должно быть отражено в таблице blog_post.

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

Бонус: миграция данных

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

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

Допустим, мы хотим иметь следующие варианты отзывов: Интересно, Умеренно интересно, Неинтересно и Скучно. Мы могли бы поместить нашу миграцию данных в тот же файл миграции, который мы создали ранее, но давайте создадим другой файл миграции специально для этой миграции данных:

./manage.py makemigrations blog --empty

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

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

# Generated by Django 2.2 on 2019-04-22 02:07

from django.db import migrations

class Migration(migrations.Migration):

    dependencies = [
        ('blog', '0002_auto_20190421_1931'),
    ]

    operations = [
    ]

Теперь мы воспользуемся операцией RunPython для выполнения функции, которая позволит нам заполнить таблицу.

Наш файл миграции должен выглядеть следующим образом:

# Generated by Django 2.2 on 2019-04-22 02:07

from django.db import migrations

initial_options = (
    ('interesting', 'Interesting'),
    ('mildly-interesting', 'Mildly Interesting'),
    ('not-interesting', 'Not Interesting'),
    ('boring', 'Boring'),
)

def populate_feedback_options(apps, schema_editor):
    FeedbackOption = apps.get_model('blog', 'FeedbackOption')
    FeedbackOption.objects.bulk_create(
        FeedbackOption(slug=slug, option=option) for slug, option in initial_options
    )


def remove_feedback_options(apps, schema_editor):
    FeedbackOption = apps.get_model('blog', 'FeedbackOption')
    slugs = {slug for slug, _ in initial_options}
    FeedbackOption.objects.filter(slug__in=slugs).delete()


class Migration(migrations.Migration):

    dependencies = [
        ('blog', '0002_auto_20190421_1931'),
    ]

    operations = [
        migrations.RunPython(
            populate_feedback_options, remove_feedback_options
        )
    ]

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

Далее, давайте применим нашу миграцию: ./manage.py migrate

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

Откат примера базы данных Django

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

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

Чтобы вернуться к исходному состоянию, выполните:

./manage.py migrate blog 0001_initial

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

Что дальше?

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

Добавление: Жизнь без инструмента миграции

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

1. Изменение контракта: изменение схемы базы данных и кода приложения

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

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

2. Планируйте изменения: напишите сценарий миграции на SQL

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

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

3. Выполнение: развертывание кода и запуск миграции в унисон

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

Отслеживание изменений без Django

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

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

Откат назад без Django

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

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

Вернуться на верх