Skip to content

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

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

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

C Web Framework предоставляет CLI-утилиту migrate, которая компилируется вместе с cpdy и позволяет:

  • Создавать новые миграции.
  • Применять миграции.

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

Как это работает

Каждая миграция — это исходный файл на C, который компилируется в разделяемую библиотеку (.so) и загружается утилитой migrate во время выполнения. Внутри файла определяется единственная функция:

c
int up(const char* dbid);

Параметр dbid — это идентификатор базы данных из config.json (например, postgresql.p1). Функция должна выполнить SQL-операторы через dbquery и вернуть 1 в случае успеха либо 0 при неудаче. Успешно применённая миграция записывается в служебную таблицу migration, чтобы не выполняться повторно.

Отката миграций (команды down) не предусмотрено: утилита поддерживает только создание и применение. Это сознательное решение — проектировайте миграции так, чтобы их можно было безопасно «накатывать» поверх предыдущих, не рассчитывая на откат.

Создание миграций

Чтобы создать новую миграцию, выполните команду create:

bash
./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, содержащий готовый шаблон:

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:

c
#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:

bash
# применить все новые миграции
./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.p1PostgreSQLdatabases.postgresql[].host_id = "p1"
mysql.m1MySQLdatabases.mysql[].host_id = "m1"
redis.r1Redisdatabases.redis[].host_id = "r1"
sqlite.db1SQLitedatabases.sqlite[].host_id = "db1"

Миграция применяется ровно к той базе, dbid которой вы передали в команде up.

Набор миграций (server_id)

server_id (s1, s2, …) — это поддиректория с независимым набором миграций (компилируются отдельно, см. app/migrations/<server_id>/). Позволяет вести несколько независимых цепочек миграций в одном проекте.

Таблица migration

При первом запуске up утилита автоматически создаёт в целевой базе служебную таблицу migration. Она содержит как минимум:

КолонкаТипНазначение
versiontextимя файла миграции (уникальный идентификатор версии)
apply_timebigintметка времени Unix применения

Перед выполнением миграции утилита проверяет наличие её version в этой таблице — уже применённые миграции пропускаются, что делает запуск up all идемпотентным и безопасным для повторного вызова.

Выпущено под лицензией MIT.