26 мая 2018
Тэги: Hibernate, Java, PostgreSQL, rest, Spring Boot, Spring Data.
В статье Hibernate и Spring Boot мы рассматривали использование Hibernate для того, чтобы не писать sql-запросы в слое доступа к данным. Сегодня мы пойдём ещё дальше и рассмотрим, как Spring Data может генерировать за вас сам слой доступа к данным со всеми методами, которые вам нужны в сервисном слое.
В качестве примера возьмём сущность «Страна» с её названием в качестве единственного параметра и на примере этой сущности шаг за шагом создадим все необходимые операции для поиска, добавления, редактирования и удаления этой сущности. В СУБД postgres надо создать следующую таблицу:
Теперь создадим типовой maven-проект и добавим в pom.xml необходимые зависимости. Полную версию файла можно посмотреть на github.
spring-boot-starter-web отвечает за обработку http-запросов, а spring-boot-starter-data-jpa предоставляет функционал доступа к данным. Также мы добавляем драйвер для работы с целевой СУБД.
Точка запуска приложения имеет стандартный для Spring Boot вид:
Параметр scanBasePackages говорит Spring, какие пакеты сканировать для поиска и создания бинов.
Начнём со слоя доступа к данным. В Hibernate сущность, которая мапится на нашу таблицу, будет выглядеть так:
@Entity говорит о том, что это – класс-сущность. @Table позволяет в явном виде задать имя таблицы в БД, но если имя класса совпадает с названием таблицы, то эту аннотацию можно не указывать. @Id указывает на поле, которое будет идентификатором сущности. В таблице это поле должно быть помечено как primary key. @GeneratedValue позволяет задать способ генерации этого идентификатора. В нашем случае мы используем то значение, которое будет присваиваться в БД. Поле name совпадает с названием поля в базе, поэтому явно имя колонки указывать не требуется.
Spring Data предоставляет абстракцию CrudRepository, которая типизируется целевой сущностью и её id. CrudRepository имеет набор базовых методов для работы с сущностью, названия которых говорят сами за себя:
Нам достаточно расширить этот интерфейс, добавить сигнатуры необходимых нам методов и Spring автоматически создаст реализацию этого интерфейса!
Для того, чтобы Spring создал реализацию этих методов, требуется соблюдать определённый порядок именования. Хоть в базовом интерфейс уже имеется метод findAll(), мы хотим задать сортировку элементов так, чтобы более поздние записи (с большим id) оказывались в начале списка. Для этого называем наш новый метод findAllByOrderByIdDesc(). Также мы хотим искать страну не только по id, но и по её имени, поэтому добавляем метод findByName().
Как уже упоминалось выше, достаточно создать интерфейс без реализации, назвать новые методы определённым образом и мы уже можем подставлять наше dao в сервисный слой при помощи @Autowired.
Начнём с метода получения всех стран. Его реализация довольно проста: мы просто вызываем целевой метод у нашего dao,
Обработчик входящих http-запросов (контроллер) будет выглядеть следующим образом:
@RequestMapping задаёт базовый адрес, по которому можно обращаться к целевым методам, а параметр produces указывает, что ответ мы всегда будем возвращать в формате json. Далее мы внедряем наш сервисный слой через @Autowired. Аннотация @GetMapping указывает, что данный метод обрабатывает GET-запрос, а также задаёт дополнительный сегмент адреса.
Параметры подключения к БД нужно вынести в отдельный файл и либо положить его в папку resources нашего проекта, либо указывать при запуске в командной строке через параметр --spring.config.location. Spring поддерживает несколько форматов настроек, но я предлагаю использовать yaml:
В настройках в секции datasource мы должны указать драйвер для работы с целевой СУБД, полный путь до БД, логин и пароль. В секции jpa мы указываем соответствующий диалект, который учитывает все особенности нашей СУБД, а также включаем вывод генерируемых sql-запросов в консоль при помощи параметра show_sql.
Теперь если мы выполним get-запрос по адресу http://127.0.0.1:8080/api/countries/, то получим список всех записей, которые есть в таблице country.
Добавим в CountryServiceImpl ещё два метода:
В методе getByName() для поиска по имени мы вызываем наш одноимённый метод из dao-слоя. В методе getById() для поиска по номеру записи вызываем стандартный метод findOne(), который ищет запись по указанному id, а если такой записи нет, то возвращает null. В случае, если запись не найдена, мы кидаем исключение NotFoundException.
Реализация этого исключения довольно проста. Аннотация @ResponseStatus позволяет указать, какой http-статус вернуть в случае ошибки. Согласно rest-архитектуре, если запись не найдена, следует возвращать код 404.
Добавим новые обработчики в контроллер:
Поскольку оба запроса принимают по одному параметру, разделим их по типу через регулярное выражение. id записи у нас всегда числовое, о чём говорит регулярка \d+. А имя у нас всегда «не число», о чём говорит выражение \D+. @PathVariable позволяет получать именованную часть урла и подставлять её как параметр метода.
Теперь мы можем выполнять поиск страны, добавляя к урлу http://127.0.0.1:8080/api/countries/ номер искомой страны или её имя. Если же запись не найдена, получим соответствующий ответ в формате json и http-статус 404.
В rest-архитектуре создание записи производится через POST-запрос, а обновление – через PUT. При этом параметры запроса можно передавать в теле этого запроса. В нашем случае единственным параметром запроса на создание новой страны будет её имя. Для этого создадим класс, в который будет мапится входящий json.
Тогда запросы на добавление и обновление страны в нашем сервисе будут выглядеть так:
CrudRepository для добавления и обновления предоставляет один метод – save(). Однако при обновлении мы получаем id целевой записи, поэтому должны предварительно проверить, что такая запись действительно есть. Для этого используем стандартный метод exists(). Если запись не найдена – кидаем исключение. Иначе создаём новый объект Country и инициализируем его соответствующими значениями.
В контроллер добавим следующие методы:
@PostMapping и @PutMapping отвечают за связь с POST- и PUT-запросами соответственно, а @RequestBody связывает тело входящего запроса с нужным классом.
Теперь мы можем добавлять новые страны следующим образом:
В ответ мы получим сущность с id, которое было присвоено ей при вставке в таблицу.
Удаление записи производится через стандартный метод delete(), но перед этим мы проверяем существование записи с таким id и если не находим, то кидаем исключение:
В контроллер добавим такой метод:
@DeleteMapping указывает на обработчик DELETE-запросов, а @ResponseStatus говорит, что в случае успеха вернём 204 http-статус – «No content».
Теперь мы можем удалять записи из таблицы, если знаем id.
Как видите, при помощи абстракции CrudRepository мы получили всю базовую функциональность для работы с сущностью в БД и при этом не написали ни одного sql-запроса.
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.