1. Схема БД как часть продукта
Если вы сейчас думаете: «Ну схема же уже есть, Hibernate её создал, всё работает — зачем трогать?», вы мыслите абсолютно по-человечески. На старте проекта так и должно быть: хочется быстрее увидеть, как save() превращается в INSERT, а не писать километры DDL. Но вот что меняется к 23‑му дню.
Пока проект был маленьким, схема действительно могла быть чем-то «на фоне»: одна-две таблицы, пару колонок добавили — и ладно. Сейчас схема уже перестала быть фоном. У нас есть несколько подсистем (catalog, inventory, ordering), связи, индексы под чтение, и самое главное — ожидания к данным. Когда проект становится таким, любая «случайная» правка базы начинает жить собственной жизнью, и довольно быстро вы обнаружите, что у вас не один проект, а два: Java-код в Git и какая-то загадочная «реальная база на вашей машине».
Это и есть момент, когда схема БД перестаёт быть «побочным эффектом запуска приложения» и становится частью продукта наравне с кодом. Примерно как application.yml: никто в здравом уме не держит конфиг только у себя в голове и не передаёт его устно с фразой «ну ты там поставь такой же URL». Со схемой ровно то же самое — просто она долго притворялась «не вашим делом».
Схема в локальной базе: риски
Схема «живёт в локальной базе», когда главный источник правды — это текущее состояние конкретной PostgreSQL на вашем ноутбуке. Не в репозитории, не в виде набора файлов, не как воспроизводимая история изменений, а как нечто, что получилось исторически. Сегодня оно такое, завтра вы «чуть-чуть поправили колонку», послезавтра добавили индекс, а потом забыли, что делали.
Самое неприятное в этом подходе даже не то, что вы забудете. Самое неприятное, что у вас появляется эффект «снежинки»: база становится уникальной, красивой, неповторимой… и бесполезной для команды. Вы не можете гарантировать, что чистая база на другой машине окажется в таком же состоянии. Вы не можете быстро поднять окружение в CI. Вы не можете нормально переключаться между ветками, потому что в одной ветке «уже есть колонка», а в другой «ещё нет, но база-то помнит». И да, рано или поздно это выльется в классический инженерный мем: «у меня работает».
Ирония в том, что проблема почти никогда не проявляется сразу. Она проявляется в момент, когда вы уже устали, хотите просто закоммитить задачу и пойти пить чай. Именно тогда база решит напомнить, что она тоже «участник проекта» и у неё есть своё мнение.
2. Ручные изменения через SQL-консоль: быстро, но без истории
Ручной ALTER TABLE — это как подкрутить винтик отвёрткой, когда скрипит дверь. В моменте приятно: скрип исчез, вы победили. Но если вы потом не записали, какой винтик и насколько подкрутили, завтра дверь начнёт скрипеть снова — уже у другого человека.
Например, вы решили добавить заметку к товару. Логично: поле небольшое, хочется хранить что-то вроде «поставщик просил не показывать на витрине до пятницы» (да, такое тоже бывает в жизни, а не только в учебниках). Вы открываете SQL-консоль и делаете так:
-- Добавляем новую колонку в таблицу: это изменение «живёт» только в вашей БД,
-- пока вы не оформите его как миграцию в репозитории.
alter table product add column note varchar(255);
И всё. На вашей базе колонка есть, вы в код добавляете поле, перезапускаете — и счастливый разработчик идёт дальше.
А теперь сюжетный поворот: ваш коллега (или вы завтра, но уже после docker compose down -v) запускает проект на чистой базе. И обнаруживает, что проект «вроде бы тот же», но колонка note отсутствует. Падает старт приложения, либо падает первый запрос на чтение/запись. И вы внезапно превращаетесь из «человека, который добавил полезную фичу», в «человека, который оставил квест на выживание».
С точки зрения Git у вас всё идеально: код коммитнут, сборка зелёная, тесты (если они есть) прошли. Но база — это не Git. Она не знает, что вы делали. Её память — это состояние. А состояние без истории плохо переносится между людьми и машинами.
3. ddl-auto=update: учебный режим и риски
Сейчас мы подходим к той теме, где очень легко сказать «Hibernate плохой» — и очень легко ошибиться. Hibernate не плохой. Hibernate просто честно делает то, что вы попросили. Проблема в том, что именно вы его попросили сделать.
На раннем этапе вы могли использовать что-то вроде:
spring:
jpa:
hibernate:
# Учебный режим: Hibernate сам пытается «догнать» схему под entity при старте.
# Удобно на демо, но опасно как постоянная стратегия в растущем проекте.
ddl-auto: update
В учебном режиме это даёт быстрый результат: вы меняете entity — и при старте приложения Hibernate пытается привести схему базы в соответствие. Удобно? Да. Быстро? Да. Снимает страх «я ничего не понимаю в DDL»? Тоже да.
Но как только проект перестаёт быть «просто демо», у этого режима появляются неприятные свойства.
Во-первых, изменения происходят во время старта приложения. Это означает, что вы запускаете сервис — и он в этот момент может менять базу. Это уже не похоже на контролируемый процесс, больше похоже на «само решило и сделало». В небольшой учебной базе это кажется безобидным, а в реальной — это прямое приглашение к хаосу.
Во-вторых, ddl-auto=update не даёт вам читаемой истории. Вы не можете открыть репозиторий и увидеть: «А, вот когда добавили колонку note и почему». У вас нет файла, который можно посмотреть на code review, обсудить, откатить (хотя откат миграций — отдельная история), применить на чистой базе в CI.
В-третьих, такие изменения сложно синхронизировать между ветками. Представьте, что вы переключились на другую ветку, где entity ещё не содержит note. Hibernate не обязан «убирать колонку» обратно (а иногда и хорошо, что не обязан). В итоге база начинает содержать следы сразу нескольких веток разработки, и вы получаете «археологический слой» из колонок, индексов и ограничений, которые непонятно откуда взялись и нужны ли ещё.
Самая коварная часть тут в том, что ddl-auto=update выглядит как «решение проблемы миграций». Мол, зачем Flyway, если Hibernate сам обновляет? Ответ будет простым и немного грустным: потому что обновлять схему — это не только «добавить колонку», а ещё и управлять изменениями как процессом, а не как побочным эффектом запуска приложения.
4. Рассинхрон кода и базы
Давайте сделаем ситуацию максимально приземлённой. Вы добавили колонку руками, и в коде тоже добавили поле:
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
@Entity
class Product {
@Column(name = "note") // Маппинг ожидает, что в таблице product существует колонка note
private String note; // Если колонку «забыли» добавить миграцией — будет рассинхрон
// остальные поля опустим
}
На вашей базе это работает, потому что вы уже сделали ALTER TABLE.
Но если завтра кто-то поднимет чистую базу (или вы сами удалите volume), то Hibernate при ddl-auto=none или ddl-auto=validate скажет: «Колонки нет». И это, честно говоря, лучшее, что могло случиться, потому что падение на старте — это мягкое предупреждение. Гораздо хуже, когда приложение стартует, а ошибка всплывает в середине бизнес-операции.
Обычно рассинхрон проявляется тремя способами.
Первый — приложение не стартует, потому что схемная валидация обнаружила несовпадение mapping’а и таблиц. Это самый честный сценарий: сломалось рано, мы быстро поняли, что не так.
Второй — приложение стартует, но при первом INSERT или SELECT начинает падать, потому что SQL, который генерирует Hibernate, обращается к несуществующей колонке. В этот момент вы получаете что-то из серии «column note does not exist». Понимать такую ошибку уже сложнее, особенно новичку: кажется, что «я же написал Java-код, при чём тут колонка».
Третий — ещё хуже: приложение как-то работает, но у вас разные окружения имеют разные схемы, и баги появляются только «у Пети в dev», «в CI», «на другой машине» или «на стенде». Вот это уже тот уровень боли, когда люди начинают ненавидеть базы данных как класс… хотя база тут вообще ни при чём.
Важно поймать мысль: JPA mapping — это ожидание к структуре базы, а не сама структура. Mapping — это карта, а база — территория. Можно нарисовать карту с мостом через реку, но если на местности мост ещё не построили, вы всё равно намокнете.
5. Схема в репозитории: один источник правды
Когда мы говорим «схема должна жить в репозитории», мы не имеем в виду «в репозитории должна быть распечатка SQL на 300 страниц». Мы имеем в виду простую инженерную дисциплину: любое изменение схемы должно быть выражено в файле, лежащем в Git, и применяемом предсказуемым способом.
Это сразу меняет жизнь проекта.
Во-первых, новый разработчик (или вы на новом ноутбуке) получает проект, запускает стандартный набор команд, и база поднимается в нужном состоянии сама. Не потому что «у всех так настроено», а потому что репозиторий содержит историю изменений. И да, это один из редких случаев, когда фраза «сама» означает «по понятным правилам, которые лежат рядом с кодом».
Во-вторых, изменения схемы становятся видимыми. Их можно обсуждать, ревьюить, спорить, улучшать. То есть схема перестаёт быть магией и возвращается в нормальный инженерный процесс.
В-третьих, появляется повторяемость. И это не абстрактное слово из книжек, а конкретная способность: «удалить базу и поднять заново без паники». Если вы можете это сделать — проект живёт. Если не можете — проект держится на удаче и памяти конкретного человека.
Чтобы закрепить ощущение, давайте сравним три подхода не «кто круче», а по инженерным свойствам:
| Подход к схеме | Что является источником правды | Можно ли поднять схему на чистой БД одинаково у всех | Можно ли ревьюить изменения | Риск “works on my machine” |
|---|---|---|---|---|
| Ручные правки в БД | Состояние вашей локальной базы | Нет (или «ну вроде да, если вы мне напишете список команд») | Нет | Очень высокий |
| ddl-auto=update | Сочетание entity-кода + текущего состояния БД | Иногда, но непредсказуемо при ветках/ручных правках | Частично (код ревьюится, но DDL — нет) | Средний → высокий по мере роста проекта |
| Миграции в репозитории | Набор файлов изменений схемы | Да | Да | Низкий (если не нарушать дисциплину) |
Обратите внимание: мы сейчас ещё не обсуждаем конкретный инструмент. Мы обсуждаем принцип: один источник правды и воспроизводимая история. Инструмент (Flyway) появится следующим шагом как механизм, который умеет применять эту историю и хранить факт её применения.
Как это связано с нашим shop-data-jpa прямо сейчас
Наш проект уже достаточно сложный, чтобы «схема как побочный эффект» стала опасной. У нас есть таблицы каталога, таблицы заказов, таблицы остатков, и связи между ними. Мы уже делали запросы, ловили N+1, оптимизировали чтение. Это означает, что мы начали думать о данных всерьёз. А значит, схема базы — больше не “времянка”.
Представьте простую цель: студент или новый разработчик клонирует репозиторий, поднимает PostgreSQL через docker compose up, запускает приложение — и получает рабочий проект. Не «рабочий после того, как он руками создаст таблицы», и не «рабочий если он сначала импортирует чей-то дамп». Рабочий просто потому, что репозиторий содержит всё нужное.
Если схема живёт только в локальной базе, вы не можете гарантировать эту цель. Более того, вы даже себе не можете её гарантировать: достаточно один раз снести volume, поменять ветку или переехать на другую машину — и вы снова будете «восстанавливать цивилизацию из руин» с фразой «кажется, тут была таблица product…».
И ещё один важный момент, который кажется мелочью, пока не станет болью. Мы скоро будем усиливать проект на уровне схемы: добавлять индексы, закладывать ограничения, вводить audit-поля. Это всё неизбежные изменения. И если вы не переведёте схему в режим «живёт в репозитории», каждое такое изменение будет превращаться в мини-детектив: «А мы это уже добавляли? А у кого в базе есть? А почему у меня работает, а у тебя нет?»
Проверка зрелости: убить базу и поднять заново
Есть очень простой мысленный эксперимент, который помогает понять, пора ли вам миграции. Он звучит жестоко, но честно: представьте, что ваша база исчезла. Например, вы случайно сделали:
# Удаляет контейнеры и volume с данными/схемой: после этого БД будет «чистой».
docker compose down -v
И всё, volume с данными и схемой улетел в цифровой рай.
Если после этого вы можете сказать «ну ок, подниму заново, схема восстановится сама, потому что она описана в проекте» — вы на правильном пути. Если вы начинаете вспоминать: «так, я где-то руками создавал таблицу, потом добавлял колонку, потом индекс…» — значит, источник правды у вас в голове и в конкретной базе, а это плохое место для хранения истины.
В учебном проекте этот эксперимент особенно полезен, потому что он быстро показывает, что именно вы строите. Вы строите backend‑проект с управляемой схемой или вы строите набор Java-классов, которые “на вашем ноутбуке как-то работают”.
И да, это нормально, что на раннем этапе у вас второй вариант. Наша задача сейчас — аккуратно перейти к первому.
6. Типичные ошибки при схеме в локальной базе
Ошибка №1: считать, что «раз приложение стартует, значит всё хорошо».
Старт приложения на вашей машине говорит только одно: на вашей машине ваша база совпала с вашим кодом в текущий момент времени. Он вообще ничего не гарантирует для чистой базы, другой машины, другой ветки и CI. Чтобы было «хорошо», схема должна быть поднимаемой из репозитория в одинаковое состояние.
Ошибка №2: оставить ddl-auto=update как постоянный режим “потому что удобно”.
Удобство update похоже на привычку хранить важные документы «на рабочем столе» — сначала кажется, что так быстрее. Потом рабочий стол превращается в археологический музей, а документ, который нужен прямо сейчас, почему-то не находится. ddl-auto может быть мостом на старте, но не должен быть фундаментом проекта.
Ошибка №3: делать ручной ALTER TABLE, а потом «забывать» превратить его в часть проекта.
Ручной SQL сам по себе не является преступлением. Проблема начинается, когда он остаётся только в истории вашей локальной базы. Если вы сделали ручную правку, она должна получить «официальный статус» в репозитории, иначе вы просто добавили невидимую зависимость: чтобы проект работал, кто-то должен повторить ваш ручной шаг.
Ошибка №4: держать два источника правды одновременно и надеяться, что они “как-нибудь договорятся”.
Когда часть схемы живёт в миграциях (или хотя бы в осознанных DDL-скриптах), а часть — в ddl-auto=update и ручных изменениях, проект превращается в игру «угадай, кто последний трогал базу». Схема должна иметь один главный источник правды, иначе вы будете постоянно ловить дрейф и странные ошибки на ровном месте.
Ошибка №5: игнорировать тот факт, что схема — это тоже код, только SQL.
Иногда кажется, что настоящий код — это Java, а SQL — так, «инфраструктурщина». Но в data-layer проекте SQL-схема — это часть контракта системы. Если вы относитесь к ней как к чему-то вторичному, система обязательно воспользуется этим и начнёт ломаться в самых неприятных местах.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ