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

Spring Data Rest с примерами на kotlin

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

30 апреля 2023

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

Содержание

  1. Подключаем Spring Data Rest
  2. Маппинг полей таблицы
  3. Создание репозитория
  4. Просмотр записей
  5. Создание новой записи
  6. Редактирование записи
  7. Удаление записи
  8. Итоги

Ранее я уже приводил пример в статье CrudRepository на Kotlin, как Spring Data позволяет легко выполнять основные операции над сущностями в БД. Теперь пойдём ещё дальше и рассмотрим как Spring Data Rest позволяет избежать написания контроллеров и сервисной логики. Поскольку писать будем на Kotlin, нам придётся учитывать специфику взаимодействия с Java-кодом, на который изначально был рассчитан Spring Data Rest.

Исходники тестового проекта, адаптированного под Spring Boot 3, доступны на github. Также доступен видеогайд.

Подключаем Spring Data Rest

Для начала создадим заготовку проекта. Проще всего это сделать с помощью Spring Initializr. В настройках выбираем в качестве языка Kotlin и в качестве сборщика Gradle – Kotlin. В dependency нам нужно последовательно добавить три зависимости: Spring Data JPA, Rest Repositories и PostgreSQL Driver. В итоге файл build.gradle.kts должен содержать следующие зависимости:

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-data-rest")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    runtimeOnly("org.postgresql:postgresql")
}

Компонент spring-boot-starter-data-jpa отвечает за работу с БД и предоставляет аннотации для разметки сущностей. spring-boot-starter-data-rest автоматически генерирует rest-контроллеры для доступа к данным. org.postgresql – драйвер СУБД (вы можете использовать другой). Обратите внимание, что в gradle он объявлен как runtimeOnly зависимость, т.к. для компиляции проекта драйвер не требуется. Мы задействуем его в декларативном стиле в настройках проекта.

Теперь найдём в проекте файл настроек application.properties (пока он пустой) и переименуем его в application.yml, чтобы указывать настройки нашего приложения в yaml формате. Настройки можно указывать в любом формате, подробнее см. статью Сравнение форматов конфига в Spring Boot.

В файле настроек нам нужно указать параметры подключения к нашей БД postgres.

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

Вам в этих настройках надо заменить только url, username и password на свои собственные. Параметр show_sql позволяет выводить в консоль все sql запросы, которые будет генерировать Spring Data.

Маппинг полей таблицы

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

create table band
(
    id serial not null constraint band_pk primary key,
    name varchar(50) not null,
    players_count integer not null,
    created date not null
);

Замапим эту таблицу в класс сущности на kotlin:

@Entity
// @Table(name = "band")
class Band(
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        var id: Int?,
        var name: String,
        var playersCount: Int,
        var created: LocalDate
)

Здесь было бы логичнее использовать data class, но из-за особенностей реализации Hibernate под капотом, нам нужно использовать обычный класс, причём все поля в нём должны быть помечены не val, а var – т.е. доступные для изменения. Иначе на каждое изменения Hibernate будет создавать новую копию класса и расходовать много дополнительной памяти.

Аннотация @Entity определяет, что перед нами класс, поля которого мапятся на поля таблицы. По имени этого класса автоматически определяется имя таблицы. Если имя таблицы отличается от имени класса, то её имя можно указать с помощью аннотации @Table. Идентификатор записи, по которому Spring Data определяет уникальность, задаём с помощью аннотации @Id. Поскольку для каждой новой записи это значение автоматически инкрементируется (увеличивается на 1) средствами БД, также надо указать аннотацию @GeneratedValue. Остальные поля, аналогично таблице, мапятся по своим именам. Причём имя с нижним подчёркиванием (т.н. «snake_case») автоматически преобразуется в привычный нам «camelCase». Если же имя колонки отличается от имени поля в классе, можно использовать для явного указания имени аннотацию @Column.

Создание репозитория

Теперь создадим репозиторий, который будет работать с этой сущностью. Причём он одновременно будет предоставлять функциональность также и rest-контроллера. И для этого нам потребуется лишь одна аннотация и один интерфейс (даже без реализации).

@RepositoryRestResource(path = "bands")
interface BandRepository : PagingAndSortingRepository<Band, Int?>

Аннотация @RepositoryRestResource определяет, что этот интерфейс является и репозиторием, и rest-контроллером. Сам репозиторий наследуется от интерфейса PagingAndSortingRepository и типизируется нашей сущностью Band. Этот базовый интерфейс автоматически добавляет функционал сортировки и постраничного вывода записей. Если вам такой функционал не нужен, можете наследоваться от более базового CrudRepository. Также важно, чтобы интерфейс типизировался именно nullable Int, который под капотом будет преобразован в java-эквивалент Integer.

В этом интерфейсе нам потребуется определить минимум 3 метода (для поиска по id, для изменения записей и для их удаления). Если бы мы писали код на Java, такие действия не потребовались бы.

fun findById(id: Int?): Optional<Band>
fun save(band: Band): Band
fun deleteById(id: Int?)

В этих методах мы также должны использовать nullable Int. Иначе вы будете получать ошибку «Method not allowed» при выполнении rest-запросов.

Собственно говоря, это всё, что нужно написать в коде. Теперь запускаем приложение.

Просмотр записей

Если мы выполним GET-запрос по адресу 127.0.0.1:8080, то в ответ получим следующий json:

{
    "_links": {
        "bands": {
            "href": "http://127.0.0.1:8080/bands{?page,size,sort}",
            "templated": true
        },
        "profile": {
            "href": "http://127.0.0.1:8080/profile"
        }
    }
}

Это специальный формат HATEOAS, который помимо самих данных содержит также и ссылки на них, что удобно для отображения на frontend. В данном случае мы видим, что у нас имеется эндпоинт /bands c тремя опциональными параметрами page, size и sort.

Чтобы получить список всех записей, выполним GET-запрос по урлу 127.0.0.1:8080/bands. Если у нас не пустая таблица, то ответ будет примерно таким:

{
    "_embedded": {
        "bands": [
            {
                "name": "Queen",
                "playersCount": 4,
                "created": "1973-07-13",
                "_links": {
                    "self": {
                        "href": "http://127.0.0.1:8080/bands/1"
                    },
                    "band": {
                        "href": "http://127.0.0.1:8080/bands/1"
                    }
                }
            }
        ]
    },
    "_links": {
        "self": {
            "href": "http://127.0.0.1:8080/bands"
        },
        "profile": {
            "href": "http://127.0.0.1:8080/profile/bands"
        }
    },
    "page": {
        "size": 20,
        "totalElements": 5,
        "totalPages": 1,
        "number": 0
    }
}

Тут мы видим все поля каждой записи в таблице, ссылки на каждую запись отдельно (по сути просто к bands добавляется id записи) и в конце списка общую информацию о том, сколько всего элементов, сколько страниц, каков размер страницы и её номер (начинается с нуля). Если мы хотим выводить по две записи на страницу и отобразить вторую страницу, то следует выполнить такой GET-запрос: 127.0.0.1:8080/bands?page=1&size=2.

Для просмотра отдельной записи просто переходим по нужной ссылке. Например, если запись имеет id = 2, то выполняем GET-запрос 127.0.0.1:8080/bands/2. В ответ получим примерно следующее:

{
    "name": "Scorpions",
    "playersCount": 5,
    "created": "1965-01-01",
    "_links": {
        "self": {
            "href": "http://127.0.0.1:8080/bands/2"
        },
        "band": {
            "href": "http://127.0.0.1:8080/bands/2"
        }
    }
}

Если вы получаете ошибку «Method not allowed» – вернитесь к предыдущему разделу.

Создание новой записи

Если мы хотим добавить в таблицу новую запись, то нужно выполнить POST-запрос на урл 127.0.0.1:8080/bands с указанием в теле запроса всех полей, кроме id. Также не забудьте передать заголовок «Content-Type: application/json». В нашем примере запрос может выглядеть так:

{
    "name": "Beatles",
    "playersCount": 4,
    "created": "1960-01-01"
}

id новой записи можно узнать, если повторно посмотреть список всех записей 127.0.0.1:8080/bands.

Редактирование записи

Если мы хотим отредактировать одно или несколько полей, не обязательно указывать запись целиком. Достаточно передать только изменяемые поля. Например, если хотим отредактировать количество участников и название, просто передаём эти значения в соответствующих полях через PATCH-запрос на урл конкретной записи 127.0.0.1:8080/bands/2:

{
    "name": "Scorpions mod",
    "playersCount": 42
}

Если значение хотя бы одного поля отличается от значения существующей записи – будет выполнен sql-запрос update. Это вы можете отследить в логах консоли при включённом параметре show_sql в application.yml.

Удаление записи

Для удаления записи достаточно кинуть на урл конкретной записи DELETE-запрос. В нашем примере это урл 127.0.0.1:8080/bands/2.

Итоги

Как видите, Spring Data Rest позволяет обеспечить полный набор операций по работе с записями в БД при минимальных трудозатратах на написание кода. Однако если вы пишете не на Java, а на Kotlin, то потребуется несколько дополнительных изменений в коде.

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



Комментарии

30.04.2023 16:53 devmark

Статья и пример проекта по работе со Spring Data Rest актуализированы для Spring Boot 3.
https://github.com/devmarkru/spring-data-rest-example

Видео будет обновлено позднее.

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

×

devmark.ru