6 января 2018
Тэги: Java 8, PostgreSQL, rest, Spring Boot, SQL.
Данная статья является продолжением Spring Boot Restful Service, где была бы раскрыта тема работы с БД в Spring Boot. Давайте рассмотрим эту тему подробнее на примере СУБД postgresql, а в качестве основы возьмём проект, который мы делали в той статье.
Напомню, что проект представляет из себя простой restful-service, который принимает GET-запрос по HTTP и возвращает профиль пользователя по его id. Сам профиль содержит кроме id также имя, фамилию и возраст. Поэтому создадим таблицу profiles в базе данных.
Для поля id можно использовать тип serial. Он представляет собой целое число, которое инкрементируется (увеличивается на 1) автоматически при вставке новой записи в таблицу.
При работе с БД нужно использовать пул подключений к БД, чтобы не создавать их заново при каждом новом sql-запросе, иначе выполнение запроса будет занимать продолжительное время. В качестве пула предлагаю использовать один из наиболее производительных в настоящий момент HikariCP. Также нам нужна поддержка работы с БД со стороны Spring Boot и драйвер для работы с СУБД postgresql. Добавим все эти зависимости в наш проект.
При инициализации пула требуется указать параметры подключения к БД, такие как логин, пароль и т.п. Поскольку данные параметры являются изменяемыми и доступ к ним должен быть ограничен, вынесем их в отдельный текстовый файл и назовём его application.config. Пример содержимого такого файла:
Чтобы Spring Boot увидел данные настройки, абсолютный путь к файлу следует указывать через параметр командной строки --spring.config.location=/путь/до/файла/application.config. Если запускаете проект при помощи Idea, указывайте данный параметр в строке Program Arguments.
Для удобства работы с этими настройками создадим класс ConnectionSettings, в который Spring автоматически подставит все настройки с префиксом «mainPool» в соответствующие поля, благодаря аннотации @ConfigurationProperties. Вообще это очень хорошая практика - группировать связанные настройки через префикс.
Для каждого из этих полей нужно создать геттер и сеттер, но я для краткости не стал их здесь приводить.
Наш пул подключений максимум может хранить до 5 объектов, однако это значение может быть переопределено через файл настроек.
Теперь создадим ещё один компонент, в котором будем инициализировать сам пул.
Аннотация @Bean позволяет нам вручную создавать бины, которые Spring потом сможет подставлять в другие компоненты.
Для работы с БД принято выделять отдельной слой dao (data access object - объект доступа к данным). Как и для сервиса из предыдущей статьи, здесь будет удобно выделить интерфейс, который будет выглядеть так:
Обратите внимание, что при поиске по id здесь мы будем возвращать типизированный Optional. То есть объект может быть в базе, а может и не быть. И в зависимости от кейса это может трактоваться как ошибка, так и нормальное поведение. Решение о том, ошибка это или нет, будет принимать сервисный слой, который мы рассмотрим далее.
Реализация класса Profile предельно проста. Его единственное назначение - это отображать поля таблицы в поля класса на Java. Для краткости не буду приводить код всего класса, ибо он достаточно прост.
Реализация dao:
Обратите внимание, что ВСЕ dao-компоненты снабжаются аннотацией @Repository, которая является частным случаем @Component. Она обеспечивает маппинг ошибок, специфичных для СУБД, в стандартные исключения JDBC.
Сам SQL-запрос для выборки профиля пользователя здесь вынесен в качестве константы в начало класса. Для подстановки целевого id используется именованный параметр с двоеточием в начале, а не простая конкатенация строки и числа. Это позволяет нам сделать запрос более устойчивым к хакерским атакам типа sql injection с одной стороны и более производительным с другой, т.к. СУБД сможет закешировать шаблон данного запроса.
NamedParameterJdbcTemplate - стандартный компонент, предоставляющий методы для взаимодействия с БД. Как видно из названия, он поддерживает именованные параметры. ProfileMapper преобразует данные, полученные из БД в объект Profile. То есть он хранит в себе логику маппинга полей таблицы на поля класса. Более подробно мы рассмотрим его чуть ниже.
Реализация нашего целевого метода getProfileById() предельно проста. Сначала подставляем требуемый id в sql-запрос через именованный параметр благодаря классу MapSqlParameterSource. Затем вызываем метод queryForObject, передавая ему сам sql-запрос, именованные параметры и маппер полей таблицы. В качестве результата получаем объект Profile или исключение EmptyResultDataAccessException если объект не найден. Исходя из того, что id является первичным ключом в таблице и его значение уникально, мы можем здесь использовать метод queryForObject(). Если бы искали не по уникальному значению, то использовали бы метод query(), который возвращает список объектов. Результат оборачиваем в Optional.
Сам ProfileMapper не хранит внутреннего состояния и всего лишь реализует интерфейс RowMapper, типизированный нашим объектом Profile.
На вход он получает ResultSet, представляющий собой результат выборки. Из этого ResultSet мы извлекаем значения полей благодаря методам getInt() и getString() по имени колонки в таблице.
Теперь осталось только внедрить наш ProfileDao в сервисный слой. В предыдущей статье мы уже создавали реализацию сервисного слоя ProfileServiceMock, которая является заглушкой и на самом деле ни в какую базу не ходит. Сейчас мы создадим другую реализацию того же сервиса:
Обратите внимание на аннотацию @Primary. Если её не указывать, то спринг не сможет заинжектить в ProfileController нужную нам реализацию сервиса, т.к. по факту у нас их две. Чтобы указать, что по умолчанию нам нужна именно эта реализация, мы и используем данную аннотацию.
Как я уже говорил, именно сервисный слой находится в контексте выполнения запроса и может правильно трактовать пустой результат из dao. В данном случае это ошибка и здесь Optional предоставляет очень удобный метод orElseThrow(), в который мы передаём наше исключение через лямбда-выражение.
На этом примере с двумя реализациями одного интерфейса хорошо виден принцип модульности, которого стоит придерживаться при разработке любых приложений на Spring.
ProfileService, в свою очередь, вызывается из контроллера. Таким образом, вырисовывается типичная трёхслойная архитектура: контроллер (с аннотацией @Controller) -> сервис (@Service) -> dao (@Repository). Контроллер отвечает за маппинг входящих http-запросов, сервисный слой реализует бизнес-логику, а dao работает непосредственно с БД.
Теперь если вы запустите приложение и выполните GET-запрос по адресу http://localhost:8080/profile/1, то получите профиль с id = 1:
Если же выполнить запрос с другим id, то наш ErrorController корректно обработает исключение ProfileNotFoundException и выдаст пользователю json с описанием ошибки:
В результате мы добавили в наше приложение слой dao, который ходит в БД, а также создали новую реализацию сервисного слоя, который вместо заглушки теперь использует это dao.
Kotlin, Java, Java 16, Java 11, Java 10, Java 9, Java 8, 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.