Миграции баз данных
Миграции позволяют версионировать структуру базы данных вместе с исходным кодом приложения. Поскольку изменение схемы базы данных почти всегда требует согласованного изменения кода, фреймворк рассматривает миграции как часть системы контроля версий проекта.
Типичные сценарии: добавить новую таблицу на этапе разработки, создать индекс для ускорения запросов уже после выхода в продакшен, переименовать колонку, привести данные в соответствие с новой схемой.
C Web Framework предоставляет CLI-утилиту migrate, которая компилируется вместе с cpdy и позволяет:
- Создавать новые миграции.
- Применять миграции.
Миграции могут не только менять схему базы данных, но и преобразовывать сами данные — внутри миграции доступен полный слой БД.
Как это работает
Каждая миграция — это исходный файл на C, который компилируется в разделяемую библиотеку (.so) и загружается утилитой migrate во время выполнения. Внутри файла определяется единственная функция:
int up(const char* dbid);Параметр dbid — это идентификатор базы данных из config.json (например, postgresql.p1). Функция должна выполнить SQL-операторы через dbquery и вернуть 1 в случае успеха либо 0 при неудаче. Успешно применённая миграция записывается в служебную таблицу migration, чтобы не выполняться повторно.
Отката миграций (команды
down) не предусмотрено: утилита поддерживает только создание и применение. Это сознательное решение — проектировайте миграции так, чтобы их можно было безопасно «накатывать» поверх предыдущих, не рассчитывая на откат.
Создание миграций
Чтобы создать новую миграцию, выполните команду create:
./exec/migrate create add_users_table ../config.json ../app/migrations/s1| Аргумент | Описание |
|---|---|
add_users_table | название миграции (используется в имени файла) |
../config.json | путь до конфигурационного файла |
../app/migrations/s1 | целевая директория, где будет создан файл миграции |
В указанной директории появится файл с именем YYYY-MM-DD_HH-MM-SS_add_users_table.c, содержащий готовый шаблон:
#include <stdlib.h>
#include "dbquery.h"
#include "dbresult.h"
int up(const char* dbid) {
dbresult_t* result = dbquery(dbid, "", NULL);
int res = dbresult_ok(result);
dbresult_free(result);
return res;
}Заполните тело up() SQL-операторами, изменяющими структуру базы данных. Параметры запроса передаются третьим аргументом dbquery — массивом array_t* (либо NULL, если параметров нет). Пример миграции, создающей таблицу news:
#include <stdlib.h>
#include "dbquery.h"
#include "dbresult.h"
int up(const char* dbid) {
dbresult_t* result = dbquery(dbid,
"CREATE TABLE news"
"("
"id bigserial NOT NULL PRIMARY KEY, "
"name varchar(100) NOT NULL DEFAULT '', "
"text text NOT NULL DEFAULT '' "
")"
, NULL);
int res = dbresult_ok(result);
dbresult_free(result);
return res;
}Имена таблиц и схемы
В примерах выше news создаётся в схеме по умолчанию. Чтобы явно указать схему (как это делают миграции проекта — public.user, public.permission), просто дополните имя таблицы: "CREATE TABLE public.news ...".
Применение миграций
Для обновления базы данных применяется команда up. Количество миграций задаётся необязательным аргументом — числом или ключевым словом all:
# применить все новые миграции
./exec/migrate up all ../config.json postgresql.p1 s1
# применить следующие 2 миграции
./exec/migrate up 2 ../config.json postgresql.p1 s1
# количество не указано — применяется 1 миграция
./exec/migrate up ../config.json postgresql.p1 s1| Аргумент | Описание |
|---|---|
up | команда применения миграции |
all | N | сколько миграций применить (all — все ожидающие, число — ровно N) |
../config.json | путь до конфигурационного файла |
postgresql.p1 | идентификатор базы данных <драйвер>.<host_id> из секции databases |
s1 | идентификатор набора миграций (поддиректория) |
Если количество не указано, применяется ровно одна миграция.
Утилита сканирует директорию ./migrations/<server_id>/ (относительно текущего рабочего каталога), загружает каждую .so через dlopen и для миграций, которых ещё нет в таблице migration, вызывает функцию up(). Каждая успешно выполненная миграция фиксируется новой строкой в migration, что позволяет различать применённые и ожидающие миграции.
Порядок выполнения
Файлы сортируются по имени по возрастанию, поэтому миграции применяются строго в порядке их создания (по временной метке в имени файла).
Идентификаторы
База данных (dbid)
Формат <драйвер>.<host_id>, где host_id берётся из config.json:
| dbid | Драйвер | Запись в config.json |
|---|---|---|
postgresql.p1 | PostgreSQL | databases.postgresql[].host_id = "p1" |
mysql.m1 | MySQL | databases.mysql[].host_id = "m1" |
redis.r1 | Redis | databases.redis[].host_id = "r1" |
sqlite.db1 | SQLite | databases.sqlite[].host_id = "db1" |
Миграция применяется ровно к той базе, dbid которой вы передали в команде up.
Набор миграций (server_id)
server_id (s1, s2, …) — это поддиректория с независимым набором миграций (компилируются отдельно, см. app/migrations/<server_id>/). Позволяет вести несколько независимых цепочек миграций в одном проекте.
Таблица migration
При первом запуске up утилита автоматически создаёт в целевой базе служебную таблицу migration. Она содержит как минимум:
| Колонка | Тип | Назначение |
|---|---|---|
version | text | имя файла миграции (уникальный идентификатор версии) |
apply_time | bigint | метка времени Unix применения |
Перед выполнением миграции утилита проверяет наличие её version в этой таблице — уже применённые миграции пропускаются, что делает запуск up all идемпотентным и безопасным для повторного вызова.