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

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

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

24 марта 2023

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

Содержание

  1. Репозиторий для дочерней сущности
  2. Создание дочерних сущностей
  3. Обновление дочерних сущностей
  4. Удаление дочерних сущностей
  5. Выводы

В предыдущей статье Spring Data JPA, REST и Kotlin: "один-ко-многим", чтение данных мы добавили к родительской сущности «Страна» дочернюю сущность «Город». В итоге у нас получилось отношение «один-ко-многим». В продолжение этой темы научимся создавать, изменять и удалять города вместе со странами в рамках одного запроса от клиента.

Репозиторий для дочерней сущности

Ранее мы уже создали сущность для города под названием 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,
)

Поскольку теперь мы хотим не только считывать информацию о городе вместе со страной, но и изменять её, нам нужно создать новый репозиторий CityRepository:

interface CityRepository: CrudRepository<CityEntity, Int> {
    fun deleteAllByCountry(country: CountryEntity)
}

Этот репозиторий мы тоже наследуем от стандартного CrudRepository, типизируя его сущностью CityEntity. В интерфейс репозитория добавляем один кастомный метод deleteAllByCountry(), который будет удалять из таблицы city все города, связанные с указанной страной. Название этого метода соответствует соглашениям об именовании, поэтому Spring Data автоматически сгенерирует его реализацию.

Создание дочерних сущностей

В сервисном слое CountryServiceImpl у нас уже есть метод create(), принимающий в качестве параметра dto (data transfer object) со страной и городами.

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

Доработаем метод создания, чтобы он сохранял также и города.

@Transactional
override fun create(dto: CountryDto): Int {
    val countryEntity = countryRepository.save(dto.toEntity())
    val cities = dto.cities.map { it.toEntity(countryEntity) }
    cityRepository.saveAll(cities)
    return countryEntity.id
}

Здесь мы сначала создаём новую страну. Репозиторий стран возвращает нам созданную сущность, в которой уже будет сгенерированный базой id. Затем мы проходимся по всем городам, пришедшим от клиента внутри dto и преобразуем их в сущность CityEntity с помощью метода расширения CityDto.toEntity(). Затем передаём полученный список сущностей городов в метод saveAll() репозитория городов (его нужно добавить в конструктор сервиса). Поскольку в данном методе несколько обращений к БД, то все запросы выполняются в рамках одной транзакции благодаря аннотации @Transactional.

Вспомогательный метод расширения выглядит так:

private fun CityDto.toEntity(country: CountryEntity): CityEntity =
    CityEntity(
        id = 0,
        name = this.name,
        country = country,
    )

Теперь запускаем приложение и выполняем в Postman POST-запрос на создание новой страны и городов:

POST-запрос на создание страны и городов

В ответ мы получим id новой страны, а в БД будет добавлена одна запись в таблицу country и 2 записи в таблицу city.

Обновление дочерних сущностей

Метод update() в сервисном слое должен быть чуть хитрее. Модифицируем его так, чтобы он сначала искал страну, изменял её поля, затем удалял связанные с ней города и вставлял те, которые придут в dto.

@Transactional
override fun update(id: Int, dto: CountryDto) {
    var existingCountry = countryRepository.findByIdOrNull(id)
        ?: throw CountryNotFoundException(id)

    existingCountry.name = dto.name
    existingCountry.population = dto.population

    existingCountry = countryRepository.save(existingCountry)

    val cities = dto.cities.map { it.toEntity(existingCountry) }
    cityRepository.deleteAllByCountry(existingCountry)
    cityRepository.saveAll(cities)
}

Подход с удалением и последующей вставкой более прост, чем вычисление только тех городов, которые реально изменились. Для удаления городов мы используем добавленный нами ранее метод deleteAllByCountry(). Для вставки новых городов вызываем стандартный метод saveAll(), который доступен в нашем репозитории городов благодаря наследованию от CrudRepository. Все эти действия опять же делаем в транзакции.

Протестируем наш метод в Postman, отправив PUT-запрос:

PUT-запрос на изменение страны и связанных с ней городов

Здесь я изменил название одного из городов и добавил ещё один новый. В результате будут удалены два существующих города и добавлены три новых, которые пришли в запросе.

Удаление дочерних сущностей

Существующий метод удаления delete() в сервисном слое дорабатывается минимально:

@Transactional
override fun delete(id: Int) {
    val existingCountry = countryRepository.findByIdOrNull(id)
        ?: throw CountryNotFoundException(id)

    cityRepository.deleteAllByCountry(existingCountry) // сначала удаляем города
    countryRepository.deleteById(existingCountry.id)
}

По сути здесь мы перед удалением страны должны сначала удалить все связанные с ней города. То есть действуем в обратном порядке по сравнению со вставкой.

При выполнении DELETE-запроса тело можно вообще не указывать:

DELETE-запрос на удаление страны и городов

Запрос удаления может быть выполнен успешно только 1 раз, т.к. перед удалением мы каждый раз пытаемся проверять, существует ли указанная страна в БД.

Выводы

Для изменения дочерних объектов, связанных с родительским, мы лишь добавили новый репозиторий и немного изменили сервисный слой. Сущности, dto и контроллер вообще не менялись. И при этом мы довольно просто обеспечили изменение дочерних сущностей в рамках одного запроса благодаря тому, что сначала удаляем все существующие связанные города, а затем добавляем те, которые пришли в запросе от клиента.

А в следующей статье Spring Data JPA, REST и Kotlin: проекции мы научимся оптимизировать sql, который генерирует Spring Data JPA, чтобы выбирать только те поля, которые нам действительно нужны.



Комментарии

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

×

devmark.ru