06.01.2018
Данная статья является продолжением Spring Boot Restful Service, где была бы раскрыта тема работы с БД в Spring Boot. Давайте рассмотрим эту тему подробнее на примере СУБД postgresql, а в качестве основы возьмём проект, который мы делали в той статье.
Напомню, что проект представляет из себя простой restful-service, который принимает GET-запрос по HTTP и возвращает профиль пользователя по его id. Сам профиль содержит кроме id также имя, фамилию и возраст. Поэтому создадим таблицу profiles в базе данных.
Для поля id можно использовать тип serial. Он представляет собой целое число, которое инкрементируется (увеличивается на 1) автоматически при вставке новой записи в таблицу.
При работе с БД нужно использовать пул подключений к БД, чтобы не создавать их заново при каждом новом sql-запросе, иначе выполнение запроса будет занимать продолжительное время. В качестве пула предлагаю использовать один из наиболее производительных в настоящий момент HikariCP. Также нам нужна поддержка работы с БД со стороны Spring Boot и драйвер для работы с СУБД postgresql. Добавим все эти зависимости в наш проект.
При инициализации пула требуется указать параметры подключения к БД, такие как логин, пароль и т.п. Поскольку данные параметры являются изменяемыми и доступ к ним должен быть ограничен, вынесем их в отдельный текстовый файл и назовём его application.config. Пример содержимого такого файла:
mainPool.jdbcDriver=org.postgresql.Driver
mainPool.jdbcString=jdbc:postgresql://localhost:5432/database_name
mainPool.jdbcUser=username
mainPool.jdbcPassword=verySecretPassword
Чтобы 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.
Тэги: Spring Boot, rest, Java 8, PostgreSQL, SQL.
Функции для работы с датой и временем в Oracle
RowMapper и ResultSetExtractor в Spring Boot
Обновление записи через DELETE-запрос в Spring Boot
Обновление записи через PUT-запрос в Spring Boot
Добавление записи через POST-запрос в Spring Boot
Java rest Spring Boot Java 8 maven Collections Stream API PostgreSQL ООП головоломки Spring Apache SQL Java 9 Oracle