Эссе о разработке игр, мышлении и книгах

Миграции backend на практике

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

В основном я пишу на Python, использую реляционные БД, поэтому и инструменты буду смотреть с ориентировкой на эти технологии. Конечно, только open source. На полноту обзора не претендую.

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

Можно ли без явных миграций?

В общем-то можно.

Нужнее всего миграции там, где есть сильная формализация структур данных. Например, в реляционных БД. Если формализация слаба, то и миграции нужны не так сильно.

Для примера:

  • Возьмём документо-ориентированное хранилище.
  • Научим сервис при запуске создавать нужные коллекции документов и индексы, если их нет.
  • Будем хранить версию формата документа в нём же.
  • Научим сервис актуализировать формат документа при его загрузке из базы.
  • ????
  • Profit.

Этого должно быть достаточно. Иногда придётся делать кастомизированные скрипты для сложных преобразований данных.

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

Поэтому мне миграции нужны.

Подходы к реализации миграций

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

  • Использование DSL или SQL.
  • Наличие поддержки ЯП общего назначения, помимо SQL.
  • Методы определения порядка миграций.

DSL vs SQL

DSL предполагает разработку языка описания схемы, который транслируется в команды SQL. Реализуют DSL либо поверх ЯП общего назначения, либо в виде конфигов в популярном формате, например, YAML.

Плюс SQL в простоте и отсутствии лишних технологий. KISS как он есть.

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

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

Решения с DSL могут дополнительно предлагать:

  • ORM.
  • Автоматическое создание миграций по описанию схемы.
  • Админку.
  • Генерацию web API.

Из моего опыта:

  • Изучение абстракций над SQL не избавляет от необходимости изучать SQL.
  • Автоматические миграции удобны, когда изменения просты, но явно не серебряная пуля.
  • ORM в большинстве случаев — лишняя абстракция. При анализе сложных запросов приходится анализировать не только SQL, но и косяки ORM.
  • Автоматические админки — крутая штука. Ваш выбор, если надо быстро и дёшево дать доступ к сервису «не программистам».
  • Генерацию API по таким DSL не использовал, так как редко приходится делать чистый CRUD. Но вроде есть популярные решения. Скорее всего, с генерацией API будет та же проблема, что и с ORM — при любом осложнении придётся лезть в глубины фреймворка.

В итоге, ситуация выглядит так:

  • SQL для тех, кто:
    • любит KISS;
    • не испытывает потребности в батарейках или может найти их на стороне;
    • начинает долгий проект и готов потратиться на подготовку инфраструктуры.
  • DSL для тех, кто:
    • не предполагает долгую работу над проектом;
    • хочет очень быстро получить работающий прототип;
    • хочет готовый набор батареек, не хочет или не может возиться с их выбором;
    • хочет интроспекцию схемы данных.

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

Поддержка ЯП

Часть инструментов поддерживает только языки баз а-ля SQL, часть дополнительно поддерживает ЯП общего назначения, например — Python.

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

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

Однако поддержка ЯП упростит жизнь:

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

Определение порядка миграций

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

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

Допустим:

  1. Есть серия миграций: A, B;
  2. В ветке X, мы добавляем миграции C, D;
  3. В ветке Y, мы добавляем миграции E, F;
  4. Мы вливаем ветки X и Y в главную ветку.

Какой порядок миграций должен быть в итоге?

  • A, B, C, D, E, F;
  • A, B, E, F, C, D;
  • A, B, C, E, D, F;

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

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

Поэтому система миграций должна:

  1. Минимум: предоставлять функциональность для фиксирования порядка миграций.
  2. Очень желательно: автоматически определять проблемы в последовательности миграций.
  3. В идеале: давать инструменты для удобного разрешения конфликтов.

Используют для этого три подхода:

  • Порядок миграций указывают списком в отдельном конфиге. При конфликтах его редактируют.
  • Указывают версию миграции в имени файла и сортируют по имени ли по версии. При конфликтах приходится переименовывать файлы.
  • Указывают версию миграции и зависимости в коде миграции. При конфликтах приходится править код миграций: исправлять указатели на предыдущую и следующую.

При этом сами версии тоже могут определять по-разному:

  1. Указанием порядкового номера. При объединении веток потребуется править кучу миграций.
  2. Используют timestamp или аналог. Позволяет избежать конфликтов в простейших случаях, но перемешает последовательности миграций в сложных.
  3. Используют что-то уникальное: UUID или имя файла с описанием сути миграции. В таких случаях конфликты версий редки или невозможны.

Соответственно, идеальное решение — указывать уникальную версию и зависимости в теле миграции. Но делает так только пара систем.

Большинство завязывается на имена файлов и порядковые номера / timestamp. На мой взгляд, это плохо характеризует разработчиков этих утилит.

Существующие инструменты

Инструментов больше, чем я перечислю далее. Существуют решения на всех ЯП. Я выбирал ориентированные на Python и те, которые выглядели «софтом общего назначения».

Не искал и не рассматривал решения для «online schema migration», вроде pt-online-schema-change, так как это довольно специфические штуки и в ближайшее время мне не потребуются.

Все указанные решения я счёл живыми на май 2021 года.

Отсортированы по языкам: Python, Go, Java, всё остальное.

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

Django migrations

Часть всем известного комбайна.

  • Репозиторий.
  • Подробная документация.
  • Язык: Python.
  • Использует DSL поверх Python для описания схемы данных.
  • Миграции оформляются модулями Python.
  • Умеет:
    • сложные зависимости между миграциями;
    • сжатие истории миграций;
    • автогенерацию миграций;
    • валидацию моделей и возможность дописывать свою валидацию.
  • Имеет много батареек, включая:
    • мощную админку из коробки;
    • несколько сторонних генераторов API;
  • Поддерживает: PostgreSQL, MySQL, MariaDB, Oracle, SQLite.

Недостатки:

  • Тормознутая ORM. На миграции не влияет, но если делать что-то помимо их, то может выстрелить. Формирование запроса раньше было ужасно медленным. Теперь просто медленное.
  • Все минусы решений с DSL.

Использование Django только для миграций может выглядеть странным, но я использовал его в этой роли для серверов Toy Defense 1, 2, 3 и для микросервисов Сказки. Вполне доволен.

Migra

Цитата из README: «Like diff but for Postgres schemas».

  • Репозиторий.
  • Документация.
  • Язык: Python.
  • Вместо поддержки последовательности миграций, сравнивает желаемое и текущее состояние базы и синхронизирует их. Умеет сравнивать схемы без миграции.
  • Поддерживает: только PostgreSQL.

Недостатки:

  • Поддерживает только миграции схемы.
  • Завязывает проект на конкретную технологию. Не так страшно, так как через пару месяцев активной разработки он всё равно на неё завяжется.
  • Завязывает вас на конкретную технологию, что хуже. При выборе другой БД для нового проекта придётся искать новый софт для миграций.
  • Не поддерживает управление порядком миграций. Вы делаете копию схемы с production, делает diff с новой (желаемой) схемой и применяете его к production.
  • Не ясно как синхронизировать параллельные изменения от нескольких разработчиков.

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

Pyrseas

  • Репозиторий.
  • Документация.
  • Язык: Python.
  • Описываете схему PostgreSQL в YAML, после чего можно сравнивать схемы и делать миграции.
  • Поддерживает: только PostgreSQL.

Недостатки как у Migra.

Цитата из документации: «The Pyrseas version control tools are not designed to be the ultimate SQL database version control solution. Instead, they are aimed at assisting two or more developers or DBAs in sharing changes to the underlying database as they implement a database application.»

PGMigrate

Миграции для PostgreSQL от Yandex.

  • Репозиторий.
  • Документация.
  • Язык: Python.
  • Миграции оформляются файлами с SQL выражениями.
  • Есть функциональность SQL callbacks при применении миграций.
  • Поддерживает: только PostgreSQL.

Недостатки:

  • Порядок применения миграций определяется номером версии в имени файла.
  • Хранит урезанную историю применения миграций — идентификаторы, даже без даты.
  • Куцая документация.

«Вот оно!» — подумал я, открывая репозиторий, но нет. Утилита очень в стиле Яндекса и олимпиадного программирования: отсутствие документации, странные решения.

Alembic

Относится к SQLAlchemy примерно так, как  Django migrations к Django ORM.

SQLAlchemy, в отличие от Django ORM, не является частью фреймворка, а значит не тянет лишних зависимостей. Я с этой библиотекой не работал, поэтому не могу объективно сравнить SQLAlchemy с  Django ORM. На глазок это очень близкие по концепции и функциональности штуки.

Прочитать про функциональность SQLAlchemy можно тут.

Недостатки:

  • Все минусы решений с DSL.

Aerich

Как Alembic и Django migrations, но для Tortoise-ORM.

  • Репозиторий.
  • Документация.
  • Язык: Python.
  • Миграции оформляются файлами с SQL выражениями.
  • Умеет автогенерацию миграций.
  • Поддерживает всё, что поддерживает Tortoise-ORM.

Недостатки:

  • Мало документации.
  • Меньше функциональности, чем у Alembic и Django migrations.
  • Не понял, можно ли писать миграции на Python.
  • Все минусы решений с DSL.

Migrate

Позиционируется как универсальный инструмент, не только для реляционных БД.

Недостатки:

  • Часть баз поддерживается с оговорками и ошибками.
  • Не хранит историю миграций: gh-179, gh-510.
  • Не умеет сжимать миграции: gh-438.
  • Порядок применения миграций определяется временем создания в имени файла.

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

SQL Migrate

  • Репозиторий.
  • Документация.
  • Язык: Go.
  • Миграции оформляются файлами с SQL выражениями.
  • Умеет мигрировать несколько баз в одном проекте.
  • Поддерживает: SQLite, PostgreSQL, MySQL, MSSQL, Oracle.

Недостатки

  • Имеет проблему с «;».
  • Порядок применения миграций определяется именем файла.

Выглядит как урезанная версия Migrate.

Goose

  • Репозиторий.
  • Документация в репозитории.
  • Язык: Go.
  • Миграции пишутся SQL и на Go. Миграции на Go выполнены в виде отдельных скриптов, как я писал в предыдущем эссе.
  • Поддерживает: PostgreSQL, MySQL, SQLite3, MsSQL, RedShift.

Недостатки:

  • Имеет проблему с «;».
  • Порядок применения миграций определяется именем файлов, что приводит к проблемам.

DBMate

Цитата из README: «a lightweight, framework-agnostic database migration tool».

  • Репозиторий.
  • Документация в репозитории.
  • Язык: Go.
  • Миграции оформляются файлами с SQL выражениями.
  • Поддерживает создание и удаление баз. На мой взгляд это не должно входить в область ответственности системы миграций базы данных. Но в контексте предыдущего эссе функциональность выглядит интересной.
  • Умеет ждать доступности базы, если та, например, ещё не запустилась.
  • При применении миграций создаёт дамп новой схемы, чтобы в системе контроля версий можно было отслеживать изменения.
  • Поддерживает: MySQL, PostgreSQL, SQLite, ClickHouse.

Недостатки:

  • Порядок применения миграций определяется временем создания в имени файла.
  • Хранит урезанную историю применения миграций — идентификаторы, без даты.

SchemaHero

Цитата из README: «SchemaHero is a Kubernetes Operator for Declarative Schema Management… Database table schemas can be expressed as Kubernetes resources that can be deployed to a cluster».

  • Репозиторий.
  • Документация.
  • Язык: Go.
  • Вместо миграций описывается текущее состояние базы на YAML. ShemaHero самостоятельно определяет разницу между желаемой и реальной схемой и применяет её.
  • Поддерживает: PostgreSQL, MySQL, SQLite, Cockroachdb, Cassandra.

Недостатки:

  • Поддерживает только миграции схемы.
  • Завязывает проект на конкретную технологию. Не так страшно, так как через пару месяцев активной разработки он всё равно на неё завяжется.
  • Завязывает вас на конкретную технологию, что хуже. При выборе другой БД для нового проекта придётся искать новый софт для миграций.
  • Мало документации. Если делаете умный diff, то документации надо больше.

Flyway

Недостатки:

  • Порядок применения миграций определяется semantic version в имени файла.

Liquibase

  • Репозиторий.
  • Документация.
  • Язык: Java.
  • Миграции оформляются файлами с SQL выражениями; конфигами в формате JSON, XML, YAML; скриптами на Groovy, Clojure.
  • Можно определить callbacks на различные события во время миграций. Callbacks описываются теми же способами, что и миграции.
  • Умеет:
    • хранить подробную историю миграций, включая контрольные суммы и инициаторов миграций;
    • проверять preconditions для контроля выполнения миграций;
  • Поддерживает много баз.

Недостатки:

  • Очень в духе Java :-D
  • Имеет проблему с «;».
  • Порядок применения миграций определяется вручную в специальном файле.

Obevo

  • Репозиторий.
  • Документация.
  • Язык: Java.
  • Миграции оформляются файлами с SQL выражениями. Но файлы разбиты не по миграции на файл, а по сущности на файл. Изменения одной сущности находятся в одном файле.
  • Умеет сложные зависимости между миграциями: можно указывать явно, можно полагаться на автоопределение;
  • Поддерживает: DB2, H2, HSQLDB, Microsoft SQL Server, MongoDB, Oracle, PostgreSQL, Redshift (from Amazon), Sybase ASE, Sybase IQ.

Недостатки:

  • Выглядит как overengineered решение.
  • На мой взгляд, кривая модель организации миграций.
  • Переусложнённая документация.

Shmig

Just for fun — миграции на bash. Для тех, кто не хочет лишних зависимостей.

  • Репозиторий.
  • Документация в репозитории.
  • Язык: Bash.
  • Миграции оформляются файлами с SQL выражениями.
  • Использует консольные клиенты к базам.
  • Поддерживает: PostgreSQL, MySQL, SQLite3.

Недостатки:

  • На Bash.
  • Последний коммит в 2019 году.
  • Порядок применения миграций определяется временем создания в имени файла.

Sqitch

  • Репозиторий.
  • Документация, есть хорошие уроки-примеры.
  • Язык:Perl.
  • Миграции оформляются файлами с SQL выражениями.
  • Использует консольные клиенты к базам.
  • Умеет:
    • сложные зависимости между миграциями;
    • поддерживать зависимости между миграциями даже в разных проектах;
    • пользовательскими скриптами проверять корректность состояния базы для каждой миграции;
    • автоматически мержить планы миграций из разных веток с помощью merge драйвера для git.
  • Поддерживает: PostgreSQL, SQLite, MySQL, Oracle, Firebird, Vertica, Exasol, Snowflake.

Недостатки:

  • Perl меня пугает.

Refinery

  • Репозиторий.
  • Документация.
  • Язык: Rust.
  • Миграции оформляются файлами с SQL выражениями или модулями Rust.
  • Имеет много, но, наверно, меньше чем у Django, сторонних батареек.
  • Для идентификаторов миграций используется UUID, что исключает коллизии.
  • Поддерживает всё, что поддерживает SQLAlchemy.

Недостатки:

  • Порядок применения миграций определяется временем создания в имени файла.

Postgres migrations

Цитата из репозитория: «A PostgreSQL migration library inspired by the Stack Overflow system described in Nick Craver's blog».

  • Репозиторий.
  • Документация в репозитории.
  • Язык: TypeScript.
  • Миграции оформляются файлами с SQL выражениями или скриптами на JavaScript и дочерних языках.
  • Умеет проверять контрольные суммы миграций.
  • Поддерживает только PostgreSQL.

Недостатки:

  • Идеологически нет обратных миграций.
  • Порядок применения миграций определяется версиями в именах файлов.
  • Нет CLI.

Yuniql

  • Репозиторий.
  • Документация.
  • Язык: C#.
  • Миграции оформляются файлами с SQL выражениями.  Файлы организованы в каталоги, один каталог — одна атомарная версия базы.
  • Умеет:
    • выполнять callbacks на начало/конец миграций, etc;
    • загружать CSV файлы в таблицы, соответствующие их именам.
  • Поддерживает: Sql Server, PostgreSQL, MySQL, MariaDb, SnowFlake, RedShift, Synapse.

Недостатки:

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

Похоже, кто-то решал свои личные боли, не подумал о накопленном сообществом опыте.

Micrate

Вдохновлена Goose, часть кода портирована оттуда.

  • Репозиторий.
  • Документация.
  • Язык: Crystal.
  • Миграции оформляются файлами с SQL выражениями.
  • Поддерживает: Postgres, Mysql, SQLite3 + всё, что поддерживает crystal-db API драйвер.

Недостатки:

  • Имеет проблему с «;».
  • Порядок применения миграций определяется именем файлов
  • Мало документации.

Как выбрать систему миграций

Могу посоветовать следующие эвристики:

  1. Если надо быстро получить результат и вы не страдаете фобией тяжёлых комбайнов — берите Django.
  2. Если, по любым причинам, хотите DSL, но хотите гибкости — берите SQLAlchemy + Alembic.
  3. Если вы хотите быть очень гибким или ожидаете сложные операции над схемой, то берите Flyway
  4. Если вам не нравится Flyway и вы не боитесь Perl, берите Sqitch.
  5. Если вам не нравится Flyway и вы боитесь Perl, попробуйте Migrate или Goose.
  6. Если используете Kubernetes или хотите странного, попробуйте SchemaHero.

Список решений, не попавших в обзор