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, 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 Артур
Огромное спасибо! :)