20 марта 2023
Тэги: Collections, Kotlin, PostgreSQL, rest, Spring Boot, Spring Data, SQL, YouTube, руководство.
В предыдущей статье Spring Data JPA, REST и Kotlin: обработка ошибок мы научились менять формат ответа при возникновении ошибки. А сегодня добавим в базу данных отношение «один-ко-многим». У нас уже имеется таблица country, которая содержит страны.
Давайте создадим новую таблицу city, которая будет содержать города. И добавим между этими таблицами связь через поле country_id в таблице city. То есть несколько разных городов могут ссылаться на одну и ту же страну. Это и есть отношение «один-ко-многим».
Определение таблицы city выглядит так:
Связь между таблицами контролируется на уровне PostgreSQL благодаря ограничению city_country_fk.
А вот как может выглядеть содержимое таблицы, если мы добавим несколько городов для США (country_id = 2) и России (country_id = 3):
Теперь научим наше приложение работать с новой таблицей. Для этого добавим сущность CityEntity:
Аннотации этой сущности аналогичны тем, что мы использовали ранее для CountryEntity, за исключением поля country. На него мы повесили две аннотации. @ManyToOne как раз указывает на тип отношения между страной и городами. @JoinColumn указывает, по какой колонке из дочерней таблицы мы должны связывать её с родительской.
Напоминаю, что для сущностей Spring Data JPA мы используем не data class, а обычные классы. Это связано с особенностями реализации данной библиотеки.
В родительскую сущность CountryEntity также надо добавить связь с сущностью городов. Но при этом нужно использовать «обратную» аннотацию @OneToMany:
Её параметр mappedBy содержит название поля из дочерней сущности CityEntity, по которому нужно сделать связь.
Для города нужно создать новый класс CityDto (data transfer object). Именно в этот объект мы будем преобразовывать сущности городов перед тем, как отдать их клиентской стороне. По сути, для города нам важно знать только его название:
Также надо добавить список городов cities в уже существующий CountryDto:
В сервисный слой CountryServiceImpl добавим новый метод расширения CityEntity.toDto() для перемаппинга сущности города в dto.
И добавим его вызов в уже существующий CountryEntity.toDto():
Больше нам ничего делать не нужно. Теперь при получении списка стран или поиска конкретной страны будет происходить обращение к этому методу и Spring Data JPA будет автоматически подгружать связанные с данной страной города.
Давайте запросим информацию о стране с id = 2:
В ответ получим ещё и все города, связанные с этой страной:
Если у вас включено логирование sql-запросов, которые генерит Spring Data JPA (параметр spring.jpa.properties.hibernate.show_sql в значении true), то вы увидите, что у БД отдельно запрашиваются страны и отдельно – города. Это следствие «ленивой» инициализации списка городов. Если бы мы в коде не обращались к полю cities, то второго запроса вообще не было бы.
Подобный подход с одной стороны позволяет экономить оперативную память и запрашивать только те данные, которые реально нужны. Однако за это мы платим бОльшим количеством запросов к БД, когда могли бы всю информацию подгрузить сразу с помощью join.
Как этого добиться? Просто добавьте FetchType.EAGER в аннотацию @OneToMany в CountryEntity:
В этом случае города и страны мы будем подгружать за одно обращение к БД с использованием join.
Таким образом, мы научились вместе со странами подгружать информацию о городах. И при этом мы не вносили никаких правок ни в контроллер, ни в репозиторий – всё заработало автоматически. А в следующей статье Spring Data JPA, REST и Kotlin: "один-ко-многим", изменение данных научимся изменять дочерние сущности вместе с родительской.
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.
21.01.2024 15:38 Миша
Не понимаю почему в дто города мы только имя вводим
21.01.2024 15:49 devmark
Потому что имя города - это единственное, чего нельзя вывести автоматически.
id города - это автоинкремент, назначаемый базой данных.
country_id - это id страны, с которой связан данный город. Поскольку в этом примере работа с городами всегда происходит в контексте какой-либо страны - id страны нам всегда известно.