Миграции
Схема не описывается отдельно: она уже описана атрибутами сущностей. Команда
db migrate читает эту разметку и создаёт в базе то, чего в ней ещё нет
— таблицы, индексы, ограничения. Повторный запуск безопасен.
Что это и зачем
Миграция здесь — создание структуры базы из кода.
Проблема. Схема, живущая отдельно от сущностей, — это двойная работа и
постоянный риск расхождения: добавили свойство в класс, забыли ALTER TABLE, и
приложение падает уже в рантайме. Поднять базу с нуля на новой машине или в CI —
ручной труд, который каждый делает по-своему.
Решение. Структура описана один раз — атрибутами сущности. Команда выводит из неё DDL и применяет. Источник правды один, и это код.
Это не система версионированных миграций
Инструмент создаёт недостающее и ничего больше. Он не сравнивает схему с базой, не изменяет и не удаляет существующее, не переносит данные и не ведёт таблицу применённых версий.
Этого достаточно для первого развёртывания, для CI и для разработки. Эволюцию схемы живого прода — переименования, смену типов, перенос данных — ведут отдельным инструментом. Границы разобраны ниже.
Что нужно, чтобы таблица создалась
Три условия, и все три обязательны. Не выполнено любое — таблица просто не появится, без ошибки.
| Условие | Где | |
|---|---|---|
| 1 | Есть репозиторий с этой таблицей | Обход идёт по репозиториям, а не по сущностям |
| 2 | У сущности стоит #[Table] |
Класс без него пропускается |
| 3 | У конфигурации стоит #[Migratable] |
Без него не рассматривается вся база |
#[Migratable] // (3) the database is opted in
class MainDbConfig extends PgDbConfig { /* ... */ }
#[Table] // (2) the entity is a table
class Order { /* ... */ }
class OrderRepository extends Repository // (1) ties the two together
{
protected string $dbConfigClassName = MainDbConfig::class;
protected string $entityClassName = Order::class;
public static string $table = 'orders';
}Команда сообщает, какое из условий не выполнено:
[Project] No DB configs found — no entity has #[Table].
[Project] No migratable configs — add #[Migratable] to a DbConfig to opt in.Почему опт-ин, а не всё подряд
#[Migratable] — это заявление «схемой этой базы управляет код». Его отсутствие
означает обратное: схему ведут снаружи, и трогать её не нужно.
Так в одном проекте уживаются база, которую поднимает Winter, и база, которой владеет другая команда или другой инструмент. Второй достаточно не помечать.
Что получается
Возьмём сущность со страницы Сущности:
#[Table]
class Order
{
#[BigId]
public ?int $id = null;
#[Varchar(32)]
#[Unique]
public string $number;
#[BigInteger]
#[Index(['created_at'])]
public int $user_id;
#[SmallInteger]
public int $status = 0;
#[Decimal(12, 2)]
#[Check('total >= 0')]
public string $total;
#[Timestamp]
#[DefaultVal('NOW()')]
public string $created_at;
}Для PostgreSQL из неё получается:
CREATE TABLE public.orders (
id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
number VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
status SMALLINT NOT NULL DEFAULT 0,
total NUMERIC(12, 2) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
PRIMARY KEY (id)
);
CREATE UNIQUE INDEX orders_number_udx ON public.orders USING BTREE (number);
CREATE INDEX orders_user_id_created_at_idx ON public.orders USING BTREE (user_id, created_at);
ALTER TABLE public.orders ADD CONSTRAINT chk_orders_a0f6358602c3db66492a6ac4bcdb3862 CHECK (total >= 0);Для MySQL — тот же набор объектов на его диалекте: AUTO_INCREMENT вместо
GENERATED … AS IDENTITY, DECIMAL вместо NUMERIC, TIMESTAMP без указания
пояса, схема не используется.
Имена объектов
Если имя не задано явно, оно собирается по правилу — предсказуемо и одинаково во всех проектах:
| Объект | Шаблон | Пример |
|---|---|---|
| Первичный ключ | таблица_pkey |
orders_pkey |
| Уникальный индекс | таблица_колонки_udx |
orders_number_udx |
| Индекс | таблица_колонки_idx |
orders_user_id_created_at_idx |
| Внешний ключ | fk_таблица_колонка |
fk_orders_user_id |
Ограничение CHECK |
chk_таблица_хеш |
chk_orders_a0f63586… |
Имя CHECK содержит хеш выражения — иначе два разных ограничения на одной таблице
столкнулись бы именами. Если имя должно быть читаемым, задайте его явно: у
#[Index], #[Unique], #[ForeignKey], #[Check] есть аргумент name.
Своё имя проверяется: оно должно начинаться с буквы или подчёркивания и состоять из
букв, цифр и подчёркиваний — иначе InvalidArgumentException при разборе.
Запуск
php call db ping # is the database reachable at all
php call db sql # print the DDL, change nothing
php call db migrate # execute itНачинать стоит с db sql: он печатает ровно те операторы, которые выполнит
db migrate, но ничего не трогает. Это же удобный способ отдать DDL администратору
базы, если применять его будете не вы.
db sql — предпросмотр
Вывод сгруппирован по конфигурации, внутри — по фазам, с числом операторов в каждой:
[Project] MainConfigurationsMainDbConfig
Extensions (1)
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
Tables (3)
CREATE TABLE public.users ( … );
CREATE TABLE public.orders ( … );
Indexes (4)
CREATE UNIQUE INDEX orders_number_udx ON public.orders USING BTREE (number);
Constraints (2)
ALTER TABLE public.orders ADD CONSTRAINT fk_orders_user_id FOREIGN KEY … ;Команда подключается к базе — ей нужен драйвер, чтобы выбрать диалект, — но не
выполняет ни одного из напечатанных операторов. Флаги работают так же, как у
migrate: db sql -t покажет только таблицы.
db migrate — выполнение
Вывод построчный, по объектам:
[Project] MainMainDbConfig
Tables (3)
public.users OK
public.orders OK
public.order_items EXIST
Indexes (4)
orders_number_udx OK
orders_user_id_created_at_idx EXIST
Constraints (2)
constraint 'fk_orders_user_id' OK| Метка | Значение |
|---|---|
OK |
Объект создан |
EXIST |
Уже был — оператор пропущен, это не ошибка |
FAILED |
Не создан; текст ошибки печатается при DEBUG=true |
`FAILED` без объяснения — поставьте `DEBUG=true`
Сообщение базы показывается только в отладочном режиме, чтобы вывод команды не
раскрывал внутренности схемы там, где её запускают в общем логе. Если объект не
создался и непонятно почему — запустите с DEBUG=true.
Справочник команды
call db [command] -[flags] --[options]| Команда | Что делает |
|---|---|
ping |
проверяет связь и печатает задержку для каждой конфигурации |
sql |
печатает сгенерированный DDL, ничего не выполняя |
migrate |
выполняет его |
pool |
утилизация пулов работающего сервера — см. Пул |
Флаги (-e -s -t -i -c) и опции (--plugin=<name>, --plugins) разобраны
ниже; полный список печатает call db --help.
Порядок
Внутри одной базы операторы идут по цепочке зависимостей — каждый следующий шаг опирается на предыдущий:
1. EXTENSIONS CREATE EXTENSION IF NOT EXISTS … (только PostgreSQL)
2. SCHEMAS CREATE SCHEMA … (только PostgreSQL)
3. TABLES CREATE TABLE … + PRIMARY KEY
4. INDEXES CREATE INDEX / CREATE UNIQUE INDEX
5. CONSTRAINTS ALTER TABLE … ADD CONSTRAINT (FK, CHECK)Расширения первыми, потому что на их функции ссылаются значения по умолчанию:
DEFAULT gen_random_uuid() не создастся, пока нет pgcrypto. Схемы — до таблиц.
Индексы и ограничения — после, они ссылаются на уже существующие колонки.
Порядок между базами
Когда конфигураций несколько, очерёдность задаётся приоритетом:
use Flytachi\Winter\Ppa\Mapping\Attributes\Config\Migratable;
use Flytachi\Winter\Ppa\Mapping\Constants\MigratablePriority;
#[Migratable(MigratablePriority::High)] // first
class AuthDbConfig extends PgDbConfig { /* ... */ }
#[Migratable] // Normal — default
class MainDbConfig extends PgDbConfig { /* ... */ }
#[Migratable(MigratablePriority::Low)] // last
class AnalyticsDbConfig extends PgDbConfig { /* ... */ }Внутри одной базы порядок таблиц не настраивается — и это не проблема: внешние ключи создаются отдельной фазой, после всех таблиц, поэтому порядок их создания между собой значения не имеет.
Расширения PostgreSQL
use Flytachi\Winter\Ppa\Mapping\Attributes\Config\Extension;
#[Migratable]
#[Extension('pgcrypto')] // gen_random_uuid() for #[UuidPk]
#[Extension('postgis', version: '3.4', schema: 'gis')]
class MainDbConfig extends PgDbConfig { /* ... */ }| Аргумент | Назначение |
|---|---|
name |
Имя расширения |
version |
Требуемая версия |
schema |
Схема, в которую его установить |
cascade |
Ставить вместе с зависимостями |
Атрибут повторяемый — по одному на расширение. На не-PostgreSQL молча игнорируется.
Повторный запуск
db migrate можно запускать сколько угодно раз: существующие объекты помечаются
EXIST и пропускаются. Это работает двумя способами — где диалект позволяет,
используется IF NOT EXISTS, где нет — распознаётся код ошибки «объект уже
существует»:
| Код | Что означает |
|---|---|
42P07 (pgsql), 42S01 (mysql) |
Таблица уже есть |
42P07 (pgsql), 42000 (mysql) |
Индекс уже есть |
42P06 |
Схема уже есть |
42710 |
Ограничение уже есть |
Любой другой код — настоящая ошибка, и объект помечается FAILED.
Идемпотентность — это не сравнение схем
«Безопасно запускать повторно» означает только, что существующие объекты не будут пересозданы. Оно не означает, что база придёт в соответствие с кодом.
Добавили свойство в сущность и запустили миграцию ещё раз — таблица уже существует, поэтому её оператор пропускается целиком, и новой колонки не появится. Добавление колонок в существующую таблицу нужно делать самостоятельно.
Выборочный запуск
Флаги ограничивают набор фаз — без них выполняются все:
php call db migrate # all phases: -e -s -t -i -c
php call db migrate -t # tables only
php call db migrate -i -c # indexes and constraints only
php call db migrate -e # extensions only (pgsql)| Флаг | Фаза |
|---|---|
-e |
Расширения |
-s |
Схемы |
-t |
Таблицы |
-i |
Индексы |
-c |
Ограничения |
Разделение полезно, когда таблицы уже созданы, а индекс добавили позже: -i
выполнит только его, не трогая остального.
Плагины
По умолчанию обрабатывается сам проект. Плагины подключаются опциями:
php call db migrate --plugin=billing # one registered plugin
php call db migrate --plugins # every registered pluginЧего инструмент не делает
Границы стоит знать заранее — они объясняют, где нужен второй инструмент.
| Не делает | Что это значит на практике |
|---|---|
| Не сравнивает схему с кодом | Изменённый тип колонки или удалённое свойство останутся незамеченными |
| Не изменяет существующее | Новая колонка в существующей таблице не появится |
| Не удаляет | Ни таблиц, ни колонок, ни индексов — DROP не генерируется никогда |
| Не переносит данные | Только структура; наполнение — отдельная задача |
| Не ведёт версии | Нет таблицы применённых миграций и нет отката |
| Не оборачивает в транзакцию | Каждый оператор выполняется сам по себе; при сбое в середине часть объектов создана |
Отсюда область применения: развернуть схему с нуля — на новой машине, в CI, в контейнере при первом запуске. Для эволюции схемы работающего прода берите инструмент с версиями и откатом.
Одна база под кодом, другая под внешним инструментом
Эти подходы не исключают друг друга. Конфигурация без #[Migratable] полностью
невидима для команды — так база, которой управляет Liquibase или Phinx, остаётся
нетронутой, а служебная база проекта продолжает подниматься из кода.
Точно так же можно исключить отдельную сущность — снять с неё #[Table]. На чтение
и запись это никак не влияет: атрибут управляет только миграцией.
Дальше
- Сущности — атрибуты, из которых строится схема
- Подключение —
#[Migratable]и#[Extension] - CLI → db — все команды и флаги
db - Repository — работа с созданными таблицами