Введение в Spring Data JDBC

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Для будущих студентов курса "Java Developer. Professional" подготовили перевод полезного материала.

Также приглашаем принять участие в открытом уроке на тему
"Введение в Spring Data jdbc"


Spring Data JDBC был анонсирован в 2018 году. Целью было предоставить разработчикам более простую альтернативу JPA, продолжая при этом следовать принципам Spring Data. Подробнее узнать о мотивах, лежащих в основе проекта, вы можете в документации

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

Для быстрого старта я использовал этот шаблон.

Предварительная подготовка

Из зависимостей нам нужны data-jdbc — стартер, flyway для управления схемой и драйвер postgres для подключения к базе данных.

// build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'

    implementation 'org.flywaydb:flyway-core'

    runtimeOnly 'org.postgresql:postgresql'
}

Далее настраиваем приложение для подключения к базе данных:

# application.yml
spring:
  application:
    name: template-app
  datasource:
    url: jdbc:postgresql://localhost:5432/demo_app?currentSchema=app
    username: app_user
    password: change_me
    driver-class-name: org.postgresql.Driver

Маппинг сущностей

Для этого примера будем использовать следующую таблицу:

create table book (
    id varchar(32) not null,
    title varchar(255) not null,
    author varchar(255),
    isbn varchar(15),
    published_date date,
    page_count integer,
    primary key (id)
);

И соответствующий java-класс (обратите внимание, что @Id импортируется из org.springframework.data.annotation.Id):

// Book.java
public class Book {
    @Id
    private String id;
    private String title;
    private String author;
    private String isbn;
    private Instant publishedDate;
    private Integer pageCount;
}

Однако, если мы запустим тест:

// BookRepositoryTest.java
@Test
void canSaveBook() {
    var book = Book.builder().author("Steven Erikson").title("Gardens of the Moon").build();
    var savedBook = bookRepository.save(book);

    assertThat(savedBook.getId()).isNotBlank();
    assertThat(savedBook.getAuthor()).isEqualTo(book.getAuthor());
    assertThat(savedBook.getTitle()).isEqualTo(book.getTitle());

    assertThat(savedBook).isEqualTo(bookRepository.findById(savedBook.getId()).get());
}

То увидим ошибку — ERROR: null value in column "id" violates not-null constraint. Это происходит, потому что мы не определили ни способ генерации id ни значение по умолчанию. Поведение Spring Data JDBC в части идентификаторов немного отличается от Spring Data JPA. В нашем примере нужно определить ApplicationListener для BeforeSaveEvent:

// PersistenceConfig.java
@Bean
public ApplicationListener<BeforeSaveEvent> idGenerator() {
    return event -> {
        var entity = event.getEntity();
        if (entity instanceof Book) {
            ((Book) entity).setId(UUID.randomUUID().toString());
        }
    };
}

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

Методы запросов 

Одной из особенностей проектов Spring Data является возможность определять методы запросов в репозиториях. Spring Data JDBC использует здесь несколько иной подход. Для демонстрации определим метод запроса в BookRepository:

Optional<Book> findByTitle(String title);

И если запустим соответствующий тест:

@Test
void canFindBookByTitle() {
    var title = "Gardens of the Moon";
    var book = Book.builder().author("Steven Erikson").title(title).build();
    var savedBook = bookRepository.save(book);
    assertThat(bookRepository.findByTitle(title).get()).isEqualTo(savedBook);
}

Получим ошибку — Caused by: java.lang.IllegalStateException: No query specified on findByTitle. В настоящее время Spring Data JDBC поддерживает только явные запросы, задаваемые через @Query. Напишем sql-запрос для нашего метода:

@Query("select * from Book b where b.title = :title")
Optional<Book> findByTitle(@Param("title") String title);

Тест пройден! Не забывайте об этом при создании репозиториев.

Примечание переводчика: в Spring Data JDBC 2.0 появилась поддержка генерации запросов по именам методов

Связи

Для работы со связями Spring Data JDBC также использует другой подход. Основное отличие в том, что отсутствует ленивая загрузка. Поэтому если вам не нужна связь в сущности, то просто не добавляйте ее туда. Такой подход основан на одной из концепций предметно-ориентированного проектирования (Domain Driven Design), согласно которой сущности, которые мы загружаем, являются корнями агрегатов, поэтому проектировать надо так, чтобы корни агрегатов тянули за собой загрузку других классов.

Один-к-одному

Для связей "один-к-одному" и "один-ко-многим" используется аннотация @MappedCollection. Сначала посмотрим на "один-к-одному". Класс UserAccount будет ссылаться на Address. Вот соответствующий sql:

create table address
(
    id      varchar(36) not null,
    city    varchar(255),
    state   varchar(255),
    street  varchar(255),
    zipcode varchar(255),
    primary key (id)
);

create table user_account
(
    id         varchar(36)  not null,
    name       varchar(255) not null,
    email      varchar(255) not null,
    address_id varchar(36),
    primary key (id),
    constraint fk_user_account_address_id foreign key (address_id) references address (id)
);

Класс UserAccount выглядит примерно так:

// UserAccount.java
public class UserAccount implements GeneratedId {
    // ...other fields
    @MappedCollection(idColumn = "id")
    private Address address;
}

Здесь опущены другие поля, чтобы показать маппинг address. Значение в idColumn — это имя поля идентификатора класса Address. Обратите внимание, что в классе Address нет ссылки на класс UserAccount, поскольку агрегатом является UserAccount. Это продемонстрировано в тесте:

//UserAccountRepositoryTest.java
@Test
void canSaveUserWithAddress() {
    var address = stubAddress();
    var newUser = stubUser(address);

    var savedUser = userAccountRepository.save(newUser);

    assertThat(savedUser.getId()).isNotBlank();
    assertThat(savedUser.getAddress().getId()).isNotBlank();

    var foundUser = userAccountRepository.findById(savedUser.getId()).orElseThrow(IllegalStateException::new);
    var foundAddress = addressRepository.findById(foundUser.getAddress().getId()).orElseThrow(IllegalStateException::new);

    assertThat(foundUser).isEqualTo(savedUser);
    assertThat(foundAddress).isEqualTo(savedUser.getAddress());
}

Один-ко-многим

Вот sql, который будем использовать для демонстрации связи "один-ко-многим":

create table warehouse
(
    id       varchar(36) not null,
    location varchar(255),
    primary key (id)
);

create table inventory_item
(
    id        varchar(36) not null,
    name      varchar(255),
    count     integer,
    warehouse varchar(36),
    primary key (id),
    constraint fk_inventory_item_warehouse_id foreign key (warehouse) references warehouse (id)
);

В этом примере на складе (warehouse) есть много товаров/объектов (inventoryitems). Поэтому в классе Warehouse мы также будем использовать @MappedCollection для InventoryItem:

public class Warehouse {
    // ...other fields
    @MappedCollection
    Set<InventoryItem> inventoryItems = new HashSet<>();

    public void addInventoryItem(InventoryItem inventoryItem) {
        var itemWithId = inventoryItem.toBuilder().id(UUID.randomUUID().toString()).build();
        this.inventoryItems.add(itemWithId);
    }
}

public class InventoryItem {
    @Id
    private String id;
    private String name;
    private int count;
}

В этом примере мы устанавливаем поле id во вспомогательном методе addInventoryItem. Можно также определить ApplicationListener для класса Warehouse с обработкой BeforeSaveEvent, в котором установить поле id для всех InventoryItem. Вам не обязательно делать в точности так, как сделано у меня. Посмотрите тесты с демонстрацией некоторых особенностей поведения связи "один-ко-многим". Главное то, что сохранение или удаление экземпляра Warehouse влияет на соответствующие InventoryItem.

В нашем случае InventoryItem не должен знать о Warehouse. Таким образом, у этого класса есть только те поля, которые описывают его. В JPA принято делать двусторонние связи, но это может быть громоздким и провоцировать ошибки, если вы забудете поддерживать обе стороны связи. Spring Data JDBC способствует созданию только необходимых вам связей, поэтому обратная связь "многие-к-одному" здесь не используется.

Многие-к-одному и многие-ко-многим

В рамках этого руководства я не буду вдаваться в подробности о связях "многие-к-одному" или "многие ко многим". Я советую избегать связей "многие-ко-многим" и использовать их только в крайнем случае. Хотя иногда они могут быть неизбежны. Оба этих типа связей реализуются в Spring Data JDBC через ссылки на Id связанных сущностей. Поэтому имейте ввиду, что здесь вам предстоит еще немного потрудиться. 

Заключение

Если вы использовали Spring Data JPA, то большая часть из того, что я рассказал, должна быть вам знакома. Я уже упоминал ранее, что Spring Data JDBC стремится быть проще, и поэтому отсутствует ленивая загрузка. Помимо этого, отсутствует кеширование, отслеживание "грязных" объектов (dirty tracking) и сессии (session). Если в Spring Data JDBC вы загружаете объект, то он загружается полностью (включая связи) и сохраняется тогда, когда вы сохраняете его в репозиторий. Примеры, которые я показал, очень похожи на свои аналоги в JPA, но помните, что многие концепции Spring Data JPA отсутствуют в Spring Data JDBC.

В целом мне нравится Spring Data JDBC. Признаю, что это может быть не лучший выбор для всех приложений, однако я бы рекомендовал его попробовать. Как человек, который в прошлом боролся с ленивой загрузкой и dirty tracking, я ценю его простоту. Я думаю, что это хороший выбор для простых предметных областей, которые не требуют большого количества нестандартных запросов.

На этом пока все, спасибо за чтение! Надеюсь, вы нашли это руководство полезным и оно будет отправной точкой для использования Spring Data JDBC.


Подробнее о курсе "Java Developer. Professional".


Записаться на открытый урок "Введение в Spring Data jdbc".

Источник: https://habr.com/ru/company/otus/blog/531332/


Интересные статьи

Интересные статьи

В этом посте мы рассмотрим, как Spring Data Jdbc строит sql-запросы для извлечения связных сущностей. Пост рассчитан на начинающих программистов и не содержит каких-то супер хитрых веще...
Python — это один из самых распространённых языков программирования. Хотя стандартные возможности Python достаточно скромны, существует огромное количество пакетов, которые позволяют реша...
В индустрии дата-центров работа продолжается, несмотря на кризис. Например, стартап Nautilus Data Technologies недавно заявил о намерении запустить новый плавучий ДЦ. О Nautilus D...
Иногда в приложениях полезно иметь консоль для управления приложением непосредственно с сервера. Одним из чрезвычайно удобных решений данной задачи является Spring Shell. Тесты — т...
В прошлых частях цикла «Введение в SSD» мы рассказали про историю появления дисков и интерфейсов взаимодействия с накопителями. Третья часть познакомит читателя с современными форм-факторами ...