20 мая 2018
Тэги: Hibernate, Java, maven, PostgreSQL, rest, Spring Boot.
Ранее мы уже рассматривали, как работать с базой данных через jdbc в статье Работа с БД в Spring Boot на примере postgresql. А сегодня возьмём Hibernate – самый популярный фреймворк для работы с БД – и убедимся, что он значительно облегчает реализацию типовых операций над сущностями.
Предположим, в БД у нас есть две сущности: страна и город. В одной стране может быть несколько городов (отношение «один-ко-многим»). Структура таблиц выглядит примерно так:
И мы хотим совершать типовые действия над этими сущностями: просмотр всего списка, поиск по id, добавление, обновление и удаление записей. Для этого создадим типовой Spring Boot проект. В pom-файле нужно прописать следующий parent:
Зависимости также стандартные:
spring-boot-starter-web нужен для функциональности rest-контроллера, spring-boot-starter-data-jpa – для работы с БД. В качестве СУБД мы выберем postgres, поэтому нам понадобится драйвер для работы с этой СУБД.
Этот pom-файл и остальные исходники вы сможете посмотреть на github (ссылка в конце статьи).
Main-класс также абсолютно стандартный:
Параметр scanBasePackages указывает, какой пакет вместе со вложенными пакетами сканировать для создания spring-бинов.
В Hibernate класс, который мапится на таблицу в БД, называется сущностным классом (entity). Для таблицы country такой класс будет выглядеть следующим образом:
Аннотация @Entity говорит Hibernate о том, что этот класс является сущностью. Аннотация @Table позволяет указать название таблицы в БД, которая связана с этой сущностью. Если имя класса и имя таблицы совпадает, то эту аннотацию можно не указывать. @Id указывает на то поле, которое является уникальным идентификатором сущности. @GeneratedValue позволяет задать несколько различных способов генерации этого идентификатора. В нашем случае мы используем GenerationType.IDENTITY, т.к. на уровне БД используем последовательности (sequence), которые заполняют это значение при вставке новой записи автоматически.
Аннотация @OneToMany задаёт отношение «один-ко-многим» между страной и городом, т.е. в одной стране может быть несколько городов. Поэтому такая аннотация всегда вешается на коллекцию (список). Параметр mappedBy указывает поле в сущности City, которое содержит ссылку на свой родительский объект (страну). Параметр cascade указывает, что действия по модификации родительского объекта также затрагивают и все дочерние. Параметр orphanRemoval указывает, что при удалении родительского объекта нужно также удалить все «осиротевшие» дочерние объекты.
Сущность City содержит обратную аннотацию:
@ManyToOne является обратной по отношению к @OneToMany и располагается в дочерней сущности. @JoinColumn указывает имя внешнего ключа в БД, по которому происходит связь с родительской сущностью.
Также обратите внимание, что на get-методе для поля country стоит аннотация @JsonIgnore. Если её не указать, то при просмотре вложенных в страну городов в формате json будет сгенерён очень большой json из-за того, что объекты указывают друг на друга. Поскольку мы хотим видеть города вложенными в страну, то просто не будем выводить информацию о стране в дочерних сущностях.
Параметры подключения к БД указываем в файле настроек, который можно положить либо в папку resources нашего maven-проекта, либо указать путь до него в командной строке через параметр --spring.config.location. Я предлагаю хранить настройки в yaml формате в файле application.yml:
Yaml – это формат для хранения иерархии настроек. Если вам привычнее обычные текстовые файлы, добавьте вместо него равноценный файл application.properties:
Здесь мы указываем драйвер для нашей СУБД, урл самой БД, логин и пароль к ней, а также диалект Hibernate, который учитывает специфику postgres.
Создадим сервис для работы с БД:
@Repository говорит о том, что это слой взаимодействия с БД. @Transactional делает каждый публичный метод транзакционным. @PersistenceContext подставляет имеющийся в контексте выполнения EntityManager – сервис, который позволяет все основные манипуляции с сущностями.
Далее объявляем два метода: просмотр всех стран и поиск страны по её id. В первом методе мы создаём запрос, очень похожий на sql, но на самом деле это hql – Hibernate Query Language. Hql, в отличие от sql, оперирует сущностями в стиле ООП. Метод поиска по id использует стандартный метод find(), который принимает тип сущности и значение id. Как видите, чтение записей из БД происходит буквально в одну строку!
Создадим rest-контроллер, который на вход будет получать запросы в формате json:
@RequestMapping позволяет указать базовый урл, на который следует слать rest-запросы. Параметр produces в данном случае указывает, что ответы на запросы также будут в формате json. При помощи @Autowired подгружаем наш репозиторий для работы с БД.
Здесь важно заметить, что в реальных проектах между контроллером и репозиторием должен быть ещё сервисный слой с аннотацией @Service, который инкапсулирует в себе всю бизнес-логику. Но, поскольку в нашем случае это будет лишь проксирование запросов из контроллера к репозиторию, то для краткости я пропущу его.
@GetMapping помечает те методы, которые будут обрабатывать GET-запросы, а @PathVariable позволяет использовать часть урла в качестве параметра запроса (в данном случае это числовой id страны).
Давайте теперь добавим в наш репозиторий метод для добавления новой записи в БД:
Поскольку на вход вместе со страной будут приходить вложенные сущности городов, причём только с указанием их имён, мы должны сначала привязать каждый новый город к текущей стране, а затем вызвать метод persist(), передав ему родительскую сущность. Этот метод вызывается только для создания новых записей и благодаря правильно размеченным сущностям вместе с родительской в БД будут добавлены и дочерние!
В контроллер также добавим соответствующий метод:
@PostMapping указывает на обработчик POST-запросов, а @ResponseStatus задаёт http-код ответа на данный запрос. Согласно архитектуре rest это не 200, а 201 – Created. @RequestBody указывает, какой класс будет использоваться для маппинга тела запроса. В нашем случае это класс Country.
Теперь мы готовы к тому, чтобы добавить страну вместе с городами! Запустим приложением и выполним следующий post-запрос по адресу http://127.0.0.1:8080/api/countries/. Не забудьте также указать заголовок Content-Type: application/json.
Если всё сделано правильно, в двух наших таблицах появится по одной записи. Обратите внимание, что для обоих сущностей мы указываем только имена. В ответ вы получите ту же самую сущность, только с указанием id, которое было присвоено записи при добавлении в БД. Это очень удобная возможность, которая работает в Hibernate «из коробки».
Для просмотра списка всех стран с городами достаточно отправить get-запрос на адрес http://127.0.0.1:8080/api/countries/.
Теперь давайте разберёмся с обновлением. Будем считать, что при обновлении мы можем изменить название страны, а связанные с ней города каждый раз будем удалять и добавлять заново. Метод в репозитории будет выглядеть так:
Сначала пробуем подгрузить по указанному id уже существующую в БД сущность вместе с городами. Если нашли такую, то устанавливаем новое имя, затем проходимся по всем существующим городам и удаляем их при помощи метода entityManager.remove(). Заметьте, что для удаления записи достаточно передать связанный с ней объект. Затем очищаем список городов и добавляем новые, которые пришли к нам в запросе. Для каждого города вызываем метод persist(). В конце вызываем метод merge() для родительской сущности. Этот метод нужно использовать только для обновления уже существующей записи.
Добавим соответствующий метод в контроллер:
Согласно rest-архитектуре, для обновления данных используется PUT-запрос, о чём говорит аннотация @PutMapping.
Тело запроса на обновление по формату точно совпадает с запросом на добавление новой записи, но слать его нужно на урл, который будет содержать id обновляемой страны. Например, http://127.0.0.1:8080/api/countries/27.
В завершение жизненного цикла нашей записи осталось добавить метод удаления. В репозитории он будет выглядеть так:
Здесь мы сначала пытаемся найти существующую запись по её id, а затем удаляем через уже знакомый нам метод. При этом связанные со страной города также будут удалены.
На уровне контроллера метод выглядит так:
@DeleteMapping указывает на обработчик DELETE-запроса. @ResponseStatus в этом методе говорит, что в ответ на запрос будет приходить статус 204 – «No content» и пустое тело запроса. Урл запроса также содержит id удаляемой записи, как и в случае с обновлением.
Благодаря Hibernate мы реализовали основные виды операций над сущностями буквально в одну строку. А благодаря Spring Boot сделали это согласно rest-архитектуре. Аннотации @OneToMany и @ManyToOne задают связь между сущностями, что позволяет легко манипулировать всеми связанными сущностями в рамках вызова одного метода.
При этом Hibernate скрывает от вас детали sql-запроса, что делает ваш код лёгким для понимания и не зависимым от реализации конкретной СУБД. Вы легко можете перейти с postgres на mysql или oracle, поменяв буквально пару строк кода в файле настроек.
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.
24.04.2023 13:34 джеки чан
Хорошее описание. годное.