28 декабря 2023
Тэги: 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 – это драйвер для работы с соответствующей СУБД. Обратите внимание, что он имеет scope = runtime. То есть зависимость нужна только в процессе работы приложения.
Для интеграции с БД также требуется указать параметры подключения, такие как логин, пароль и т.п. Создадим файл application.yml в папке resources с примерно таким содержимым:
Если в проекте по умолчанию был файл application.properties – удалите его.
Обратите внимание, что достаточно лишь прописать параметры подключения – и всё остальное Spring Boot сделает за вас. Например, инициализирует пул подключений.
Для работы с БД принято выделять отдельный слой repository. Как и для сервиса из предыдущей статьи, здесь будет удобно выделить интерфейс:
При поиске по id здесь мы будем возвращать Optional. То есть объект может быть в базе, а может и не быть. И в зависимости от контекста это может трактоваться и как ошибка, и как нормальное поведение. Решение о том, ошибка это или нет, будет принимать сервисный слой, который мы модифицируем далее.
Реализация record-класса Profile рассматривалась в предыдущей статье. Его единственное назначение – это отображать поля таблицы в поля класса на Java.
Реализация репозитория:
Обратите внимание, что ВСЕ репозитории снабжаются аннотацией @Repository, которая является частным случаем @Component. Она обеспечивает маппинг ошибок, специфичных для СУБД, в стандартные исключения JDBC.
Сам SQL-запрос для выборки профиля пользователя здесь вынесен в качестве константы в начало класса. Для подстановки целевого id используется именованный параметр с двоеточием в начале, а не простая конкатенация строки и числа. Это позволяет нам сделать запрос более устойчивым к хакерским атакам типа sql injection с одной стороны и более производительным с другой, т.к. СУБД сможет закешировать шаблон данного запроса.
NamedParameterJdbcTemplate – стандартный компонент, предоставляющий методы для взаимодействия с БД. Как видно из названия, он поддерживает именованные параметры. ProfileMapper преобразует данные, полученные из БД в объект Profile. То есть он хранит в себе логику маппинга полей таблицы на поля класса. Более подробно мы рассмотрим его чуть ниже.
Реализация нашего целевого метода getProfileById() предельно проста. Сначала подставляем требуемый id в sql-запрос через именованный параметр благодаря классу MapSqlParameterSource. Затем вызываем метод query, передавая ему сам sql-запрос, именованные параметры и маппер полей таблицы. В качестве результата получаем список объектов типа Profile.
Исходя из того, что id является первичным ключом в таблице и его значение уникально, мы можем здесь использовать преобразование в стрим и findFirst(). Такая связка более универсальна и безопасна, чем queryForObject(), который может выкинуть исключение, если найдено более одной записи.
Сам ProfileMapper не хранит внутреннего состояния и всего лишь реализует интерфейс RowMapper, типизированный нашим объектом Profile.
На вход он получает ResultSet, представляющий собой результат выборки. Из этого ResultSet мы извлекаем значения полей благодаря методам getInt() и getString() по имени колонки в таблице.
Теперь осталось только внедрить наш ProfileDao в сервисный слой. В предыдущей статье мы уже создавали реализацию сервисного слоя ProfileServiceMock, которая является заглушкой и на самом деле ни в какую базу не ходит. Сейчас мы создадим другую реализацию того же сервиса:
Обратите внимание на аннотацию @Primary. Если её не указывать, то спринг не сможет заинжектить в ProfileController нужную нам реализацию сервиса, т.к. по факту у нас их две. Чтобы указать, что по умолчанию нам нужна именно эта реализация, мы и используем данную аннотацию. Ещё разные реализации интерфейса можно подставлять в зависимости от профиля приложения с помощью аннотации @Profile.
Как я уже говорил, именно сервисный слой находится в контексте выполнения запроса и может правильно трактовать пустой результат из репозитория. В данном случае это ошибка и здесь Optional предоставляет очень удобный метод orElseThrow(), в который мы передаём наше исключение через лямбда-выражение.
На этом примере с двумя реализациями одного интерфейса хорошо виден принцип модульности, которого стоит придерживаться при разработке любых приложений на Spring.
ProfileService, в свою очередь, вызывается из контроллера. Таким образом, вырисовывается типичная трёхслойная архитектура: контроллер (с аннотацией @Controller) -> сервис (@Service) -> dao (@Repository). Контроллер отвечает за маппинг входящих http-запросов, сервисный слой реализует бизнес-логику, а dao работает непосредственно с БД.
Теперь если вы запустите приложение и выполните GET-запрос по адресу http://localhost:8080/profiles/1, то получите профиль с id = 1:
Если же выполнить запрос с другим id, то наш ErrorController корректно обработает исключение ProfileNotFoundException и выдаст пользователю json с описанием ошибки:
В результате мы добавили в наше приложение слой взаимодействия с БД, а также создали новую реализацию сервисного слоя, который вместо заглушки теперь использует репозиторий.
В следующей статье Добавление записи через POST-запрос в Spring Boot мы научимся создавать записи в БД.
Kotlin, Java, Spring, Spring Boot, Spring Data, SQL, PostgreSQL, Oracle, Linux, Hibernate, Collections, Stream API, многопоточность, файлы, 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 Артур
Огромное спасибо! :)