8 марта 2026
Тэги: Java, PostgreSQL, rest, Spring Boot, SQL, Stream API, руководство.
Данная статья является продолжением Spring Boot Restful Service, где была бы раскрыта тема работы с БД в Spring Boot. Давайте рассмотрим эту тему подробнее на примере СУБД postgresql, а в качестве основы возьмём проект, который мы делали в той статье.
Напомню, что проект представляет из себя простой restful-service, который принимает GET-запрос по HTTP и возвращает профиль пользователя по его id.
Сам профиль содержит кроме id также имя, фамилию и возраст. Поэтому создадим таблицу profile в базе данных.
Для поля id можно использовать тип serial. Он представляет собой целое число, которое увеличивается на 1 автоматически при вставке новой записи в таблицу.
Для выполнения запросов в БД нам нужно добавить в наш проект ещё две зависимости:
spring-boot-starter-jdbc позволяет выполнять sql-запросы в базу, а postgresql - это драйвер для работы с соответствующей СУБД. Обратите внимание, что он имеет runtime scope. То есть зависимость не нужна для компиляции, а только для работы приложения.
Для интеграции с БД также требуется указать параметры подключения, такие как логин, пароль и т.п. В файл application.yaml в папке resources добавим такое содержимое:
Обратите внимание, что достаточно лишь прописать параметры подключения - и всё остальное Spring Boot сделает за вас. Например, инициализирует пул подключений.
Для работы с БД принято выделять отдельный слой репозитория. Назовём его ProfileRepository:
Обратите внимание, что ВСЕ репозитории снабжаются аннотацией @Repository, которая является частным случаем @Component. Она обеспечивает маппинг ошибок, специфичных для СУБД, в стандартные исключения JDBC.
Для взаимодействия с БД будем использовать JdbcClient, который обладает той же функциональностью, что и JdbcTemplate, но при этом предоставляет более удобное и современное API. Поэтому JdbcClient является рекомендуемой заменой JdbcTemplate.
Сам SQL-запрос для выборки профиля пользователя здесь вынесен в качестве константы в начало класса. Для подстановки целевого id используется именованный параметр с двоеточием в начале, а не простая конкатенация строки и числа. Это позволяет нам сделать запрос более устойчивым к хакерским атакам типа sql injection с одной стороны и более производительным с другой, т.к. СУБД сможет закешировать шаблон данного запроса.
При поиске по id мы будем возвращать Optional. То есть объект может быть в базе, а может и не быть. И в зависимости от контекста это может трактоваться и как ошибка, и как нормальное поведение. Решение о том, ошибка это или нет, будет принимать сервисный слой, который мы модифицируем далее.
Реализация record-класса Profile рассматривалась в предыдущей статье. Его единственное назначение - это отображать поля таблицы в поля класса на Java.
Реализация метода getProfileById() довольно простая:
Сначала в jdbcClient с помощью метода sql() указываем текст sql-запроса. Затем с помощью метода param() указываем значение параметра id. Параметров в запросе может быть больше одного и каждый из них можно указывать с помощью отдельного вызова param() или же передать сразу всю мапу с параметрами в метод params(). После этого для чтения данных вызываем метод query(), в который передаём лямбду c RowMapper. Эта лямбда принимает на вход resultSet и rowNum. RowMapper позволяет построчно обрабатывать результат запроса. Номер строки нам здесь не нужен, поэтому вместо него пишем нижнее подчёркивание, что означает безымянный параметр.
Лямбда возвращает объект Profile, заполняя его поля с помощью методов объекта ResultSet: getInt() и getString(). В качестве параметра они оба принимают имя колонки из sql-запроса.
Поскольку в этом запросе мы ищем запись по id, мы ожидаем не более одной строки. Также записи может не быть вообще. Поэтому для получения результата используем метод optional(), возвращающий соответствующий объект.
Теперь осталось только внедрить наш ProfileRepository в сервисный слой. В предыдущей статье мы уже создавали реализацию сервисного слоя ProfileServiceMock, которая является заглушкой и на самом деле ни в какую базу не ходит. Сейчас мы создадим другую («честную») реализацию того же сервиса:
Обратите внимание на аннотацию @Primary. Если её не указывать, то спринг не сможет заинжектить в ProfileController нужную нам реализацию сервиса, т.к. по факту у нас их теперь две. Чтобы указать, что по умолчанию нам нужна именно эта реализация, мы и используем данную аннотацию. Ещё разные реализации интерфейса можно подставлять в зависимости от профиля приложения с помощью аннотации @Profile.
Как я уже говорил, именно сервисный слой «знает» контекст выполнения запроса и может правильно трактовать пустой результат из репозитория. В данном случае пустой результат - это ошибка и здесь Optional предоставляет удобный метод orElseThrow(), в который мы передаём наше исключение через лямбда-выражение.
На этом примере с двумя реализациями одного интерфейса хорошо виден принцип модульности, которого стоит придерживаться при разработке любых приложений на Spring.
ProfileService, в свою очередь, вызывается из контроллера. Таким образом, вырисовывается типичная трёхслойная архитектура: контроллер (с аннотацией @Controller) -> сервис (@Service) -> репозиторий (@Repository). Контроллер отвечает за маппинг входящих http-запросов, сервисный слой реализует бизнес-логику, а репозиторий работает непосредственно с БД.
Теперь, если вы запустите приложение и выполните GET-запрос по адресу http://localhost:8080/profiles/1, то получите профиль с id = 1:
Если же выполнить запрос с другим id, то наш ErrorController корректно обработает исключение ProfileNotFoundException и выдаст пользователю json с описанием ошибки:
В результате мы добавили в наше приложение слой взаимодействия с БД, а также создали новую реализацию сервисного слоя, который вместо заглушки теперь использует репозиторий.
В следующей статье Добавление записи через POST-запрос в Spring Boot мы научимся создавать записи в БД.
Kotlin, Java, Spring, Spring Boot, Spring Data, Spring AI, SQL, PostgreSQL, Oracle, H2, Linux, Hibernate, Collections, Stream API, многопоточность, чат-боты, нейросети, файлы, devops, Docker, Nginx, Apache, maven, gradle, JUnit, YouTube, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.
29.06.2022 13:25 Артур
Запускаю через Idea, с указанным параметром в моем случае --spring.config.location=/Users/arthurchebotkov/Desktop/SpringBootRestfulService/spring-boot-restful-service/src/main/resources/application.config
Приложение не запускается, пишет в лог:
[main] ru.devmark.app.RestfulApplication : No active profile set, falling back to default profiles: default
Подскажите, с чем может быть связано?
29.06.2022 13:29 devmark
А у вас точно не запускается? Потому что "No active profile set" - это не ошибка, а всего лишь предупреждение, что профиль не указан явно и используется профиль default.
Если профиль явно не выставлен, то приложение всё равно запустится. Но профиль также можно выставить через параметры запуска.
29.06.2022 13:46 Артур
Не запускается: ERROR 9976 --- [ main] o.s.boot.SpringApplication : Application startup failed
Насколько я понял указывает на следующие ошибки:
1) Cannot load configuration class: ru.devmark.app.RestfulApplication
2) java.lang.reflect.InaccessibleObjectException-->Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @7791a895
3) Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @7791a895
29.06.2022 15:10 devmark
А на какой версии Java вы этот пример запускаете? Изначально он создавался под Java 8. Судя по тексту ошибки, ругается на то, что в проекте недоступен модуль java.lang.
Но вообще данный пример немного устарел. У меня на YouTube канале есть актуальная серия видео как создавать restful приложение на jdbc: https://youtu.be/hta62ffKcK4. Также есть аналогичное про Spring Data https://youtu.be/BSJcA6IHIZw.
29.06.2022 16:47 Артур
Запускаю на Java 18.
Просто хотел запустить проект на Java+Maven.
Спасибо за информацию!
01.07.2022 00:26 devmark
Я переработал статью и исходники проекта под актуальную версию Spring Boot и Java 17.
02.07.2022 12:13 Артур
Огромное спасибо! :)