Статьи Утилиты Telegram YouTube Отзывы

Spring Data JPA, REST и Kotlin: "один-ко-многим", чтение данных

Видеогайд Исходники

20 марта 2023

Тэги: Collections, Kotlin, PostgreSQL, rest, Spring Boot, Spring Data, SQL, YouTube, руководство.

Содержание

  1. Дочерняя таблица city
  2. Новая сущность CityEntity
  3. Доработка CountryEntity
  4. Доработка dto
  5. Доработка сервисного слоя
  6. Ленивая инициализация

В предыдущей статье Spring Data JPA, REST и Kotlin: обработка ошибок мы научились менять формат ответа при возникновении ошибки. А сегодня добавим в базу данных отношение «один-ко-многим». У нас уже имеется таблица country, которая содержит страны.

Содержимое таблицы country

Дочерняя таблица city

Давайте создадим новую таблицу city, которая будет содержать города. И добавим между этими таблицами связь через поле country_id в таблице city. То есть несколько разных городов могут ссылаться на одну и ту же страну. Это и есть отношение «один-ко-многим».

Диаграмма таблиц country и city

Определение таблицы city выглядит так:

create table city
(
    id serial constraint city_pk primary key,
    country_id integer not null constraint city_country_fk references country,
    name varchar not null
);

Связь между таблицами контролируется на уровне PostgreSQL благодаря ограничению city_country_fk.

А вот как может выглядеть содержимое таблицы, если мы добавим несколько городов для США (country_id = 2) и России (country_id = 3):

Содержимое таблицы city

Новая сущность CityEntity

Теперь научим наше приложение работать с новой таблицей. Для этого добавим сущность CityEntity:

@Entity
@Table(name = "city")
class CityEntity (
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Int = 0,
    var name: String = "",
    @ManyToOne
    @JoinColumn(name = "country_id")
    var country: CountryEntity,
)

Аннотации этой сущности аналогичны тем, что мы использовали ранее для CountryEntity, за исключением поля country. На него мы повесили две аннотации. @ManyToOne как раз указывает на тип отношения между страной и городами. @JoinColumn указывает, по какой колонке из дочерней таблицы мы должны связывать её с родительской.

Напоминаю, что для сущностей Spring Data JPA мы используем не data class, а обычные классы. Это связано с особенностями реализации данной библиотеки.

Доработка CountryEntity

В родительскую сущность CountryEntity также надо добавить связь с сущностью городов. Но при этом нужно использовать «обратную» аннотацию @OneToMany:

@Entity
@Table(name = "country")
class CountryEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Int = 0,
    var name: String = "",
    var population: Int = 0,
    @OneToMany(mappedBy = "country")
    var cities: List<CityEntity> = emptyList(),
)

Её параметр mappedBy содержит название поля из дочерней сущности CityEntity, по которому нужно сделать связь.

Доработка dto

Для города нужно создать новый класс CityDto (data transfer object). Именно в этот объект мы будем преобразовывать сущности городов перед тем, как отдать их клиентской стороне. По сути, для города нам важно знать только его название:

data class CityDto(
    val name: String,
)

Также надо добавить список городов cities в уже существующий CountryDto:

data class CountryDto(
    val id: Int? = null,
    val name: String,
    val population: Int,
    val cities: List<CityDto>,
)

Доработка сервисного слоя

В сервисный слой CountryServiceImpl добавим новый метод расширения CityEntity.toDto() для перемаппинга сущности города в dto.

private fun CityEntity.toDto(): CityDto =
    CityDto(
        name = this.name,
    )

И добавим его вызов в уже существующий CountryEntity.toDto():

private fun CountryEntity.toDto(): CountryDto =
    CountryDto(
        id = this.id,
        name = this.name,
        population = this.population,
        cities = this.cities.map { it.toDto() },
    )

Больше нам ничего делать не нужно. Теперь при получении списка стран или поиска конкретной страны будет происходить обращение к этому методу и Spring Data JPA будет автоматически подгружать связанные с данной страной города.

Давайте запросим информацию о стране с id = 2:

curl http://127.0.0.1:8080/countries/2

В ответ получим ещё и все города, связанные с этой страной:

{
    "id": 2,
    "name": "США",
    "population": 333449281,
    "cities": [
        {
            "name": "Вашингтон"
        },
        {
            "name": "Нью-Йорк"
        },
        {
            "name": "Лос-Анджелес"
        }
    ]
}

Ленивая инициализация

Если у вас включено логирование sql-запросов, которые генерит Spring Data JPA (параметр spring.jpa.properties.hibernate.show_sql в значении true), то вы увидите, что у БД отдельно запрашиваются страны и отдельно – города. Это следствие «ленивой» инициализации списка городов. Если бы мы в коде не обращались к полю cities, то второго запроса вообще не было бы.

Подобный подход с одной стороны позволяет экономить оперативную память и запрашивать только те данные, которые реально нужны. Однако за это мы платим бОльшим количеством запросов к БД, когда могли бы всю информацию подгрузить сразу с помощью join.

Как этого добиться? Просто добавьте FetchType.EAGER в аннотацию @OneToMany в CountryEntity:

@OneToMany(mappedBy = "country", fetch = FetchType.EAGER)
var cities: List<CityEntity> = emptyList(),

В этом случае города и страны мы будем подгружать за одно обращение к БД с использованием 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 страны нам всегда известно.

Добавить комментарий

×

devmark.ru