JavaRush /Курсы /Spring Data JPA /Каркас shop-data-jpa

Каркас shop-data-jpa и smoke-тест

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

1. Стартовый каркас: что фиксируем

Каркас проекта — это набор решений, которые переживут весь курс, даже если бизнес-кода пока почти нет. Это как фундамент и стены дома: шторы потом можно менять сколько угодно, а кривой фундамент будет аукаться долго. Хороший каркас делает проект воспроизводимым, понятным и таким, который не стыдно показать тимлиду.

Живая PostgreSQL уже поднята, DataSource и JPA-инфраструктура понятны, конфиг и dev-настройки мы тоже разобрали по частям. Теперь всё это нужно собрать в один рабочий baseline. Без такой сборки любая @Entity останется просто аннотированным классом: сохраняться ей будет некуда и не через что.

На этом этапе фиксируем несколько вещей: структуру репозитория, базовый Gradle-билд, место для Docker Compose, место для конфигурации, корневой package и «зоны» проекта (catalog, inventory, ordering, common). И ещё делаем простую, почти грубую проверку подключения к базе — smoke-тест. Это не тест бизнес-логики и не «проверка JPA», а проверка уровня «шнур питания вставлен, лампочка горит».

В репозитории стоит оставить именно этот минимум: чистый main(), согласованный конфиг и один smoke-runner под dev-профиль. Разовые проверки через println(), getBean() и временные debug-конфиги полезны для понимания, но не должны расползаться по постоянному каркасу.

Чтобы было проще ориентироваться, держите в голове простую формулу цели: к концу лекции у нас есть репозиторий, который можно клонировать на другой компьютер, поднять PostgreSQL, запустить приложение и увидеть, что оно подключилось к базе, — без ручных шаманств.

2. Корень репозитория: минимальный набор файлов

В корне репозитория должны лежать файлы, которые объясняют и машине, и человеку, как жить этому проекту. Если их нет, очень быстро возникает классическая ситуация: у вас «всё работало», а у коллеги — «почему-то нет», и дальше начинается древняя магия уровня «а ты точно перезапускал?». Мы будем не магами, а инженерами, поэтому сразу раскладываем всё по местам и называем вещи своими именами.

Вот минимальный «скелет» репозитория в виде дерева. Он небольшой, но уже задаёт дисциплину:

shop-data-jpa
├─ build.gradle.kts
├─ settings.gradle.kts
├─ docker-compose.yml
├─ .gitignore
└─ src/main
   ├─ java/com/example/shopdatajpa
   │  ├─ ShopDataJpaApplication.java
   │  ├─ catalog
   │  │  └─ package-info.java
   │  ├─ inventory
   │  │  └─ package-info.java
   │  ├─ ordering
   │  │  └─ package-info.java
   │  └─ common
   │     ├─ package-info.java
   │     └─ config
   │        └─ DbConnectionSmokeTestRunner.java
   └─ resources
      ├─ application.yml
      └─ application-dev.yml

Обратите внимание: мы сознательно не создаём сейчас entity/repository/service подпакеты, потому что сегодня фиксируем инфраструктуру и каркас, а не начинаем маппинг. Но «зоны» проекта создаём сразу, чтобы код не стекался в одну папку «потом разберусь».

Чтобы это всё не выглядело как «просто набор файлов», давайте коротко зафиксируем смысл в таблице:

Файл/папка Зачем он нужен именно сейчас
docker-compose.yml Описывает PostgreSQL как часть проекта. Без него база превращается в «местный фольклор».
build.gradle.kts Фиксирует сборку, зависимости и имя проекта. Это входная дверь для Gradle.
settings.gradle.kts Задаёт имя корневого проекта и делает вывод Gradle понятным.
src/main/resources/application*.yml Место для конфигурации подключения и профилей. Никакого хардкода в Java.
src/main/java/.../ShopDataJpaApplication.java Минимальная точка входа. Должна быть чистой и скучной (это комплимент).
common/config/DbConnectionSmokeTestRunner.java Техническая проверка подключения к БД в dev-профиле.

Конфигурационный baseline проекта

Чтобы это дерево не было просто красивой схемой, в конфигурации должны совпасть несколько вещей: имя приложения, активный профиль, параметры DataSource, dev-настройки пула, SQL-видимость и явно выбранный ddl-auto.

application.yml:

spring:
  application:
    name: shop-data-jpa

  profiles:
    active: dev

application-dev.yml:

spring:
  datasource:
    url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:shop}
    username: ${DB_USER:shop}
    password: ${DB_PASSWORD:shop}
    hikari:
      maximum-pool-size: 10
      minimum-idle: 2
      connection-timeout: 2000

  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true
    hibernate:
      ddl-auto: create-drop

logging:
  level:
    org.hibernate.SQL: DEBUG
    org.hibernate.orm.jdbc.bind: TRACE

Этого достаточно, чтобы Compose-стенд и приложение говорили об одной и той же базе: shop/shop/shop, localhost:5432, dev-профиль, небольшой пул и видимый SQL. Если на вашей машине внешний порт другой, вы меняете только DB_PORT, а не расковыриваете весь конфиг. Значение create-drop здесь — именно dev-решение для раннего старта: оно удобно, пока вы осознанно принимаете поведение схемы.

settings.gradle.kts: имя проекта

Файл маленький, но приятно, когда ./gradlew projects показывает понятное имя.

// Человеческое имя корневого проекта в выводе Gradle
rootProject.name = "shop-data-jpa"

build.gradle.kts: Java 25, Spring Boot 4 и JPA-стек

Ниже пример минимального build.gradle.kts, который соответствует baseline курса (Java 25, Spring Boot 4.0.3). Здесь важно не количество зависимостей, а то, что они ровно те, которые дают нам инфраструктуру дня: JPA-стартер и драйвер PostgreSQL.

plugins {
    // Базовая поддержка Java-проекта
    java

    // Spring Boot Gradle Plugin
    id("org.springframework.boot") version "4.0.3"

    // Управление версиями транзитивных зависимостей
    id("io.spring.dependency-management") version "1.1.7"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"

java {
    toolchain {
        // Фиксируем версию Java через toolchain, чтобы сборка была воспроизводимой
        languageVersion = JavaLanguageVersion.of(25)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    // JPA-стек (Hibernate и инфраструктура репозиториев)
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")

    // Драйвер PostgreSQL нужен только на рантайме (компилятору он не требуется)
    runtimeOnly("org.postgresql:postgresql")

    // Тестовая база проекта (в курсе используем JUnit 6)
    testImplementation(platform("org.junit:junit-bom:6.0.0"))
    testImplementation("org.junit:junit-jupiter")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.withType<Test>().configureEach {
    // Запуск тестов через JUnit Platform (подходит для JUnit 6)
    useJUnitPlatform()
}

Пара коротких комментариев, чтобы это не воспринималось как «магический рецепт из интернета». runtimeOnly для драйвера означает: «на этапе компиляции он мне не нужен, но на этапе запуска без него ничего не поедет». А testImplementation мы добавляем сейчас не потому, что сегодня будем писать тесты, а потому что проекту полезно иметь нормальную тестовую базу с первого дня, даже если реально пользоваться ей начнём заметно позже.

docker-compose.yml: PostgreSQL как часть проекта

Да, это уже было в лекции 1, но сейчас мы фиксируем Compose как часть итогового каркаса. Важно не усложнять файл раньше времени: один сервис, понятные креды, volume для данных.

services:
  postgres:
    # Образ PostgreSQL, который будет подниматься вместе с проектом
    image: postgres:16
    ports:
      # Пробрасываем порт БД на хост, чтобы приложение подключалось к localhost:5432
      - "5432:5432"
    environment:
      # Простейшие креды для dev-режима (в проде так делать нельзя)
      POSTGRES_DB: shop
      POSTGRES_USER: shop
      POSTGRES_PASSWORD: shop
    volumes:
      # Данные БД сохраняются между перезапусками контейнера
      - shop-postgres-data:/var/lib/postgresql/data

volumes:
  shop-postgres-data:

Если вы смотрите на это и думаете «а можно сложнее?» — можно. Docker вообще позволяет многое. Но наша цель сегодня — воспроизводимость, а не соревнование «кто построит DevOps за вечер».

3. Пакеты: зоны вместо «папки с кашей»

Когда кода мало, очень хочется сделать один пакет com.example.shopdatajpa, свалить туда всё, а потом «как-нибудь разнести». Это примерно как складывать все документы в коробку «Разобрать»: коробка растёт быстрее, чем ваша вера в человечество. Поэтому сразу делаем по-взрослому: делим проект на зоны предметной области мини-магазина и отдельно выделяем место под общую инфраструктуру.

Мы заранее знаем три бизнес-зоны: catalog, inventory, ordering. В них позже появятся entity, repository, service, query подпакеты, но сегодня мы не забегаем вперёд. Четвёртая зона — common: сюда складываем конфигурацию, общие исключения и общую инфраструктуру. Это не «помойка для всего общего», а место для действительно общих механизмов.

Проблема новичка здесь обычно не в том, что он «плохо понимает архитектуру», а в том, что IDE позволяет создать 50 пакетов за минуту, и через неделю вы уже не помните, где что лежит. Поэтому правило простое: на старте создаём минимум, но называем его правильно.

package-info.java как «табличка на двери»

Java-пакеты физически начинают существовать для Git и проекта только тогда, когда в них есть файлы. Если просто создать пустые папки, Git может их не сохранить, и коллега после git clone скажет: «А где ваш catalog?». Самый аккуратный способ закрепить пакет — package-info.java: маленький файл, который можно воспринимать как табличку на двери комнаты.

Пример для catalog (аналогично для inventory, ordering, common):

/**
 * Каталог товаров: всё, что связано с категориями и товарами.
 * Пока это только зона проекта. Конкретные сущности появятся позже.
 */
package com.example.shopdatajpa.catalog;

Это выглядит смешно, пока проект маленький. Но когда в нём появится 40–60 классов, вы неожиданно начнёте уважать эти маленькие таблички.

Нюанс про component scanning

Spring Boot по умолчанию сканирует компоненты начиная с пакета, где лежит класс с @SpringBootApplication, и ниже по дереву. Отсюда практическое правило: корневой класс приложения должен лежать в самом верхнем пакете проекта, например com.example.shopdatajpa. Иначе однажды вы создадите компонент в com.example.shopdatajpa.ordering, а Spring его «не увидит», и вы двадцать минут будете спорить с монитором.

4. Точка входа Spring Boot: минимализм

Есть соблазн превратить main() в лабораторию: тут println(), там getBean(), ещё один кусок временного кода — и вроде бы удобно. На дистанции это превращается в грязную кухню, на которой уже невозможно готовить: всё заляпано экспериментами, и вы не понимаете, что проекту действительно нужно, а что осталось от вчерашнего любопытства. Поэтому main() делаем коротким и скучным, а технические проверки выносим в отдельный компонент.

Именно такой main() и стоит оставить в репозитории. Разовые диагностики через getBean(), println() и временные debug-конфиги полезны, но постоянную точку входа они только засоряют.

Вот нормальная минимальная точка входа:

package com.example.shopdatajpa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ShopDataJpaApplication {

    public static void main(String[] args) {
        // Точка входа должна быть «скучной»: только запуск контекста Spring
        SpringApplication.run(ShopDataJpaApplication.class, args);
    }
}

Обратите внимание, насколько тут мало «интеллекта». И это хорошо: интеллект приложения должен жить в сервисах и репозиториях, а не в точке входа. Сегодня мы ещё не пишем сервисы, но стиль фиксируем уже сейчас.

А вот технический smoke-тест мы вынесем в отдельный класс, чтобы точка входа оставалась чистой. Это не педантичность ради педантичности: через две недели вы скажете себе спасибо, потому что main() будет выглядеть как часть продукта, а не как записная книжка.

5. Smoke-тест подключения: DataSource напрямую

Smoke-тест — это минимальная техническая проверка на вопрос: «проект вообще живой?». В нашем случае вопрос ещё конкретнее: «приложение действительно может получить соединение к PostgreSQL?». Не «вроде стартануло», не «где-то там есть DataSource», а физически: взяли соединение, посмотрели метаданные, проверили валидность.

Ключевая идея: smoke-тест не должен зависеть от будущих сущностей, репозиториев, JPQL и прочего. Сегодня у нас нет таблиц, нет entity-классов, и это нормально. Мы тестируем самый нижний и честный уровень — DataSourceConnection.

Размещение smoke-теста и профиль dev

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

Вот этот runner уже имеет смысл оставить в каркасе. Он не захламляет main(), живёт только в dev и честно проверяет нижний уровень подключения.

Ниже пример файла DbConnectionSmokeTestRunner.java:

package com.example.shopdatajpa.common.config;

import javax.sql.DataSource;

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

@Component
@Profile("dev") // Запускаем smoke-тест только в dev-профиле
public class DbConnectionSmokeTestRunner implements ApplicationRunner {

    private final DataSource dataSource;

    public DbConnectionSmokeTestRunner(DataSource dataSource) {
        // DataSource предоставляет Spring Boot (обычно через HikariCP)
        this.dataSource = dataSource;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // Берём физическое соединение и проверяем, что оно реально открывается
        try (var connection = dataSource.getConnection()) {
            // Печатаем URL, чтобы убедиться, что подключились к «той самой» базе
            System.out.println("DB url = " + connection.getMetaData().getURL());
            // DB url = jdbc:postgresql://localhost:5432/shop

            // Простая проверка валидности соединения (таймаут в секундах)
            System.out.println("DB valid = " + connection.isValid(2));
            // DB valid = true
        }
    }
}

Тут происходит ровно то, что нам нужно: Spring внедряет DataSource, мы берём соединение, печатаем URL, чтобы убедиться, что это та база, а не какая-то «случайная», и проверяем валидность соединения.

Это и есть наша постоянная техническая проверка в стартовом baseline. Всё остальное — разовые проверки, которые помогали понять отдельные куски инфраструктуры.

Польза smoke-теста vs Started...

Иногда Spring Boot действительно падает, если к БД не подключиться, но это не значит, что любая проблема проявится очевидно. И уж точно надпись Started... не равна «всё отлично». Smoke-тест полезен именно тем, что заставляет увидеть факт подключения в явном виде. Это как не просто услышать «машина завелась», а ещё и посмотреть на приборную панель, где горит «Check engine». И да, в разработке «Check engine» горит часто — просто обычно мы делаем вид, что это декоративная подсветка.

6. Запуск и проверка: порядок действий

На этом этапе важно собрать всё в одну короткую последовательность действий, которую вы сможете повторить завтра, послезавтра и на другом компьютере. Когда инфраструктура воспроизводима, учебный проект перестаёт быть лотереей. И именно здесь появляется ощущение: «я контролирую среду», а не «среда иногда милостиво разрешает мне писать код».

Последовательность простая: сначала поднимаем PostgreSQL через Compose, затем запускаем приложение с dev-профилем, потом смотрим на лог smoke-теста.

Поднимаем PostgreSQL

# Поднимаем контейнеры в фоне
docker compose up -d

# Проверяем, что postgres действительно в состоянии running
docker compose ps

Команда ps должна показать, что контейнер действительно в состоянии running. Если он не running, сначала лечим это, а уже потом открываем IDE. Да, звучит как занудство, но такое занудство экономит часы.

Запускаем приложение

Если вы запускаете из консоли:

# Запуск Spring Boot приложения через Gradle
./gradlew bootRun

Если из IDE — запускайте ShopDataJpaApplication. Главное, чтобы активировался dev-профиль, потому что smoke-тест помечен @Profile("dev").

Что увидеть в консоли

В потоке логов Spring Boot будет много служебного шума. Нас сейчас интересуют две вещи: что приложение стартовало и что smoke-тест вывел информацию о соединении.

И ещё один важный нюанс перед просмотром логов. Этот smoke-тест идёт через DataSource и JDBC: мы открываем соединение и вызываем isValid(). Поэтому Hibernate SQL-логеры могут вообще молчать — ORM на этом шаге не выполняет запросы к сущностям, и это нормальное поведение.

Если всё хорошо, вы увидите строки примерно такого вида:

DB url = jdbc:postgresql://localhost:5432/shop
DB valid = true

И это очень важные две строчки. Они означают: приложение не просто подняло контекст, оно реально открыло соединение к вашей базе shop на localhost:5432. Это и есть наш «минимальный флаг победы» на сегодня.

Маленькая схема связей

Иногда полезно один раз увидеть общую цепочку, чтобы перестать воспринимать инфраструктуру как магию. Вот схема уровня «на пальцах», но она соответствует реальности:

flowchart TD
    A[docker compose up] --> B[PostgreSQL контейнер running]
    B --> C[Spring Boot старт]
    C --> D["Boot создает DataSource (HikariCP)"]
    D --> E[DbConnectionSmokeTestRunner берет Connection]
    E --> F[Печатаем URL и isValid]

Если цепочка рвётся, вы обычно сможете понять, где именно: контейнер не поднялся; профиль не активен; неверный URL; неверный пароль; порт занят. Всё это диагностируется гораздо проще, когда у вас есть ясная цепочка, а не ощущение «оно не работает, потому что не работает».

7. Типичные ошибки при старте каркаса

Ошибки на этом этапе обычно не «про программирование», а про то, что в голове пока нет устойчивой модели: где живёт база, где живёт приложение, кто кого запускает и кто кому что должен. Это нормальная стадия обучения: вы учитесь мыслить инфраструктурой как частью кода. Давайте разберём типовые грабли так, чтобы вы узнавали их по симптомам, а не по боли.

Ошибка №1: увидеть Started ... и решить, что подключение к БД гарантированно работает.
Старт контекста и реальная проверка соединения — разные вещи. Да, чаще всего без базы JPA-инфраструктура не поднимется, но «чаще всего» не равно «всегда» и точно не равно «мне не нужна диагностика». Smoke-тест полезен именно тем, что даёт маленький, однозначный сигнал.

Ошибка №2: держать «временный» smoke-тест в main() и забыть его там навсегда.
Если main() становится местом экспериментов, вы быстро перестаёте понимать, что проект «должен делать», а что он делает потому, что вы неделю назад «проверяли одну штуку». Компонент ApplicationRunner в common.config — простой способ вынести проверку из точки входа и оставить main() чистым.

Ошибка №3: создать классы не под корневым пакетом и потом удивляться, что Spring их не видит.
Если ShopDataJpaApplication лежит в com.example.shopdatajpa, а ваш DbConnectionSmokeTestRunner внезапно оказался в com.example.common, компонент-сканирование по умолчанию его просто пропустит. Симптом будет странный: приложение стартует, а smoke-тест как будто не запускается. Решение обычно скучное: положить всё внутрь com.example.shopdatajpa.*.

Ошибка №4: перепутать порты в Compose или забыть, что схема host:container читается слева направо.
Запись "5432:5432" означает: «на хосте порт 5432 проброшен в контейнерный порт 5432». Если вы поменяли их местами или уже заняли 5432 чем-то другим, например локальным PostgreSQL на машине, приложение будет честно пытаться подключиться и получать Connection refused. Здесь лечится не Java-кодом, а настройкой стенда.

Ошибка №5: включить ddl-auto: create-drop «для удобства» и потом удивляться, что данные исчезают.
create-drop честно делает то, что написано: создаёт схему при старте и удаляет при остановке. Это удобно для короткой демки, но ужасно, если вы начинаете хранить там какие-то данные «на память». Если используете демо-режимы, делайте это осознанно и держите их в dev-профиле, чтобы случайно не привыкнуть к ним как к норме.

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