JavaRush /Курсы /Spring Data JPA /Схема БД в репозитории

Схема БД в репозитории

Spring Data JPA
23 уровень, 0 лекция
Открыта

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-схема — это часть контракта системы. Если вы относитесь к ней как к чему-то вторичному, система обязательно воспользуется этим и начнёт ломаться в самых неприятных местах.

1
Задача
Spring Data JPA, 23 уровень, 0 лекция
Недоступна
SQL-файлы вместо ручной схемы
SQL-файлы вместо ручной схемы
1
Задача
Spring Data JPA, 23 уровень, 0 лекция
Недоступна
Воспроизводимое пересоздание таблицы из Bash-скрипта
Воспроизводимое пересоздание таблицы из Bash-скрипта
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ