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, H2, Linux, Hibernate, Collections, Stream API, многопоточность, чат-боты, нейросети, файлы, devops, Docker, 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 страны нам всегда известно.