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