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, H2, Linux, Hibernate, Collections, Stream API, многопоточность, чат-боты, нейросети, файлы, devops, Docker, Nginx, Apache, maven, gradle, JUnit, YouTube, новости, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.