Статьи
Утилиты 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: "один-ко-многим", изменение данных научимся изменять дочерние сущности вместе с родительской.



Комментарии

21.01.2024 15:38 Миша

Не понимаю почему в дто города мы только имя вводим

21.01.2024 15:49 devmark

Потому что имя города - это единственное, чего нельзя вывести автоматически.
id города - это автоинкремент, назначаемый базой данных.
country_id - это id страны, с которой связан данный город. Поскольку в этом примере работа с городами всегда происходит в контексте какой-либо страны - id страны нам всегда известно.

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

×

devmark.ru