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

Spring Data JPA, REST и Kotlin: подключение к БД

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

14 марта 2023

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

Содержание

  1. Интеграция с базой данных
  2. Репозиторий
  3. Сортировка стран по алфавиту
  4. Постраничный вывод

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

Интеграция с базой данных

Теперь пришла пора подключиться к базе данных и научиться считывать данные из неё. Сначала создадим таблицу country и наполним её данными:

create table country
(
    id serial constraint country_pk primary key,
    name varchar not null,
    population integer not null
);
insert into country (id, name, population) values (1, 'Китай', 1452371069);
insert into country (id, name, population) values (2, 'США', 333449281);
insert into country (id, name, population) values (3, 'Россия', 146030881);
insert into country (id, name, population) values (4, 'Германия', 83349300);
insert into country (id, name, population) values (5, 'Франция', 68959599);
insert into country (id, name, population) values (6, 'Италия', 59236213);

В результате выполнения этих запросов наша таблица будет выглядеть так:

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

Затем в проекте в папке resources переименуем файл application.properties в application.yml и пропишем там настройки подключения к БД (здесь вам нужно указать свои настройки):

spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://localhost:5432/example_db
    username: example_user
    password: "Pa$$word"
  jpa:
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQL10Dialect
        show_sql: true
        temp:
          use_jdbc_metadata_defaults: false

driver-class-name – это класс драйвера вашей СУБД, он берётся как раз из зависимости postgres. url, username и password указываете свои собственные. В секции jpa.properties.hibernate можно указать некоторые дополнительные параметры. Например, show_sql позволяет отображать в логах sql-запросы, которые будет генерить Spring Data.

Теперь раскомментируем ранее закомментированные нами зависимости spring-boot-starter-data-jpa и org.postgresql:postgresql в файле build.gradle.kts.

Репозиторий

Создадим сущность CountryEntity, поля которой будут мапиться на поля таблицы country в базе данных.

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

Аннотация @Entity указывает на то, что перед нами именно сущность, жизненный цикл которой управляется Spring Data. Аннотация @Table в явном виде позволяет задать название таблицы, с которой связана эта сущность. Если имя таблицы и имя сущности совпадают, данную аннотацию вообще можно опустить.

Обратите внимание, что мы создаём обычный class, а не data class. Это связано с внутренней реализацией Spring Data и неизменяемые data-классы нам здесь не подходят. По той же причине все поля, которые могут менять свои значения, помечаются как var.

Аннотация @Id указывает поле, которое определяет уникальность сущности. @GeneratedValue указывает на способ присвоения значений этому идентификатору. В данном случе у нас автоинкремент, реализуемый средствами СУБД.

Перейдём к слою репозитория и создадим интерфейс CountryRepository. Наследуем его от стандартного интерфейса CrudRepostitory, который типизируется нашей сущностью и типом Int (тип поля id в сущности).

interface CountryRepository : CrudRepository<CountryEntity, Int>

И здесь начинается самое интересное, поскольку нам не нужно писать реализацию интерфейса. Spring Data сам сгенерит реализацию и sql-запросы под нашу конкретную СУБД автоматически!

Нам остаётся только внедрить этот интерфейс в сервисный слой и вызвать у него метод findAll() вместо нашего хардкода:

@Service
class CountryServiceImpl(
    private val countryRepository: CountryRepository,
) : CountryService {
    override fun getAll(): List<CountryDto> =
        countryRepository.findAll()
            .map { it.toDto() }

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

С помощью метода расширения CountryEntity.toDto() осуществляем преобразование списка сущностей базы данных в список dto, который отдаём как результат работы сервиса. На первый взгляд это может показаться избыточным, но с ростом количества полей у сущности часть из них может потребоваться скрыть от внешних пользователей. Или наоборот, добавить в dto какие-то дополнительные поля, вычисляемые динамически.

Теперь запустим наш проект и выполним тот же http-запрос, что делали ранее. В ответе мы увидим не хардкод, а именно те сущности, которые лежат в таблице country.

Сортировка стран по алфавиту

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

В данном случае его нужно назвать findByOrderByName(). Более подробно про соглашения об именовании читайте в статье Написание запросов в Spring Data JPA.

interface CountryRepository : CrudRepository<CountryEntity, Int> {
    fun findByOrderByName(): List<CountryEntity>
}

И в сервисном слое заменить вызов метода findAll().

override fun getAll(): List<CountryDto> =
    countryRepository.findByOrderByName()
        .map { it.toDto() }

Теперь снова запустим проект и убедимся, что страны отсортированы в алфавитном порядке.

Постраничный вывод

Выводить все имеющиеся в таблице страны и сортировать их по алфавиту это, конечно, удобно. Но что делать, если стран будет слишком много? Можно реализовать постраничный вывод. И Spring Data позволяет это делать с минимальными трудозатратами.

В метод репозитория добавим параметр типа Pageable.

fun findByOrderByName(pageable: Pageable): List<CountryEntity>

Очень важно, чтобы при постраничном выводе у вас была хоть какая-то сортировка. В противном случае для одной и той же страницы вы можете получать разные результаты.

В метод сервисного слоя добавим параметр pageIndex.

override fun getAll(pageIndex: Int): List<CountryDto> =
    countryRepository.findByOrderByName(PageRequest.of(pageIndex, 2))
        .map { it.toDto() }

В репозиторий мы передаём объект PageRequest, который является реализацией интерфейса Pageable. Метод PageRequest.of() принимает два параметра: номер страницы (0 – первая страница) и количество элементов на странице.

В метод контроллера также добавляем параметр pageIndex:

@GetMapping
fun getAll(@RequestParam("page") pageIndex: Int): List<CountryDto> =
    countryService.getAll(pageIndex)

Аннотация @RequestParam связывает его с параметром запроса page.

Теперь запускаем наш проект и, чтобы получить список стран на первой странице, выполняем следующий запрос:

curl http://127.0.0.1:8080/countries?page=0

Увеличивая параметр page мы постепенно можем пройтись по всем страницам. И на каждой будет не более двух стран.

В следующей статье Spring Data JPA, REST и Kotlin: поиск записей мы научимся искать страны по id и по названию.



Комментарии

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

×

devmark.ru