Этап 11 - Database migrations через Flyway и Liquibase
Backend code становится тяжелым для поддержки, когда важное поведение спрятано в случайных controller methods. Эта статья про versioned database migrations. Тема выглядит технической, но настоящий вопрос практический: что произойдет, когда реальный client отправит реальный request, а в нем что-то неполное, слишком большое, запрещенное, медленное или конфликтующее с текущим состоянием?
Database schema - часть приложения. Если developers меняют tables вручную, environments расходятся. Migrations делают schema changes repeatable, reviewable и связанными с code version.

Картинка, которую нужно держать в голове
Представь маленький online shop. User открывает frontend, нажимает кнопку, и browser вызывает backend. Backend принимает HTTP data, преобразует их в Java objects, проверяет rules, трогает database и возвращает JSON. Если у каждого шага есть понятный владелец, систему можно читать. Если все шаги смешаны в одном методе, первый production incident становится болезненным.
Для этой темы последовательность такая:
- V1 creates table.
- V2 adds column.
- V3 inserts seed data.
- Flyway runs in order.
| Часть | Ответственность |
|---|---|
| Entity | Persistence object под управлением Hibernate. |
| Transaction | Граница, где database changes commit или roll back вместе. |
| Mapper | Преобразует persistence shape в API shape. |
| Migration | Версионированное изменение базы рядом с application code. |
Конкретный Spring пример
-- V2__add_order_status.sql
ALTER TABLE orders ADD COLUMN status VARCHAR(30) NOT NULL DEFAULT 'NEW';
CREATE INDEX idx_orders_status_created_at ON orders(status, created_at);
-- V3__seed_admin_role.sql
INSERT INTO roles(name) VALUES ('ADMIN') ON CONFLICT DO NOTHING;
Код специально маленький, потому что главное здесь - граница ответственности. Controller не должен незаметно решать business rules, которые принадлежат service. Service не должен зависеть от servlet objects. Repository не должен знать, какое JSON field name ожидает frontend. Когда граница чистая, то же поведение можно документировать, тестировать и менять без правок во всех слоях.
Почему это важно
В demo project многие shortcuts выглядят безобидно. Вернуть все records нормально, пока в таблице пять строк. Вернуть entity нормально, пока в ней не появилось скрытое поле. Логировать без request id нормально, пока несколько users не падают одновременно. Работать с вручную созданной database нормально, пока staging не получает другую schema. Production work в основном состоит в том, чтобы убрать такие hidden assumptions до того, как они станут incidents.
Самое простое полезное правило: сделай behavior явным на границе, enforce его в правильном слое и держи public contract стабильным. Если frontend знает request format, response format и error behavior, он работает увереннее. Если tests проверяют тот же contract, refactoring становится безопаснее. Если logs и configuration отражают тот же design, operations меньше зависят от угадывания.
Частые ошибки
- Помещать весь use case в controller method.
- Позволять persistence details протекать в API contract.
- Обрабатывать happy path, но оставлять failure behavior неопределенным.
- Использовать local shortcut, который не сможет работать в staging или production.
Чеклист понимания
- Я могу нарисовать последовательность этой темы от request до response.
- Я могу объяснить, какой слой владеет решением.
- Я могу назвать production problem, которую эта тема предотвращает.
Вопросы для самопроверки
- Что произойдет, если каждый endpoint реализует это правило по-своему?
- Какая часть behavior должна быть задокументирована для API clients?
- Какой test докажет, что правило работает, а не только happy path?