Это четвертая часть серии заметок о реактивном программировании, в которой будет представлено введение в R2DBC и описано, как мы можем использовать Spring Data R2DBC для создания полностью реактивного приложения.

1. Что такое R2DBC?

Если вы еще не знакомы с реактивным программированием и реактивными потоками, я рекомендую вам сначала прочитать введение в реактивное программирование, в котором описывается мотивация, лежащая в основе этой парадигмы программирования.

При разработке реактивного приложения, которое должно включать доступ к реляционной базе данных, JDBC не подходит, поскольку это блокирующий API.

R2DBC означает Reactive Relational Database Connectivity и предназначен для обеспечения возможности работы с базами данных SQL с использованием полностью реактивного неблокирующего API. Он основан на спецификации Reactive Streams и в первую очередь представляет собой SPI (Service Provider Interface - интерфейс поставщика услуг) для разработчиков драйверов баз данных и авторов клиентских библиотек, то есть не предназначен для использования непосредственно в коде приложения.

На данный момент существуют реализации драйверов для Oracle, Microsoft SQL Server, MySQL, PostgreSQL, H2, MariaDB и Google Cloud Spanner.

2. SPRING DATA R2DBC

Spring Data предлагает клиент R2DBC - Spring Data R2DBC.

Это не полный ORM, как JPA - он не предлагает таких функций, как кеширование или отложенная загрузка. Но он обеспечивает функциональность отображения объектов и абстракцию репозитория.

Чтобы продемонстрировать, как его можно использовать, давайте вернемся к примеру StudentController из предыдущей заметки о WebFlux:

@RestController
@RequestMapping("/students")
public class StudentController {

    @Autowired
    private StudentService studentService;


    public StudentController() {
    }

    @GetMapping("/{id}")
    public Mono<ResponseEntity<Student>> getStudent(@PathVariable long id) {
        return studentService.findStudentById(id)
                .map(ResponseEntity::ok)
                .defaultIfEmpty(ResponseEntity.notFound().build());
    }

    @GetMapping
    public Flux<Student> listStudents(@RequestParam(name = "name", required = false) String name) {
        return studentService.findStudentsByName(name);
    }

    @PostMapping
    public Mono<Student> addNewStudent(@RequestBody Student student) {
        return studentService.addNewStudent(student);
    }

    @PutMapping("/{id}")
    public Mono<ResponseEntity<Student>> updateStudent(@PathVariable long id, @RequestBody Student student) {
        return studentService.updateStudent(id, student)
                .map(ResponseEntity::ok)
                .defaultIfEmpty(ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{id}")
    public Mono<ResponseEntity<Void>> deleteStudent(@PathVariable long id) {
        return studentService.findStudentById(id)
                .flatMap(s ->
                        studentService.deleteStudent(s)
                                .then(Mono.just(new ResponseEntity<Void>(HttpStatus.OK)))
                )
                .defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND));
    }
}

Этот контроллер содержит несколько различных методов для выполнения действий над учащимися. Мы видим, что он использует StudentService для выполнения этих действий. Теперь мы рассмотрим эту функциональность, лежащую в основе контроллера REST, и то, как мы можем реализовать доступ к базе данных с помощью R2DBC.

2.1 Пример реализации

2.1.1 Зависимости

Во-первых, нам нужно добавить в наш проект пару новых зависимостей:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-r2dbc</artifactId>
        </dependency>

        <dependency>
            <groupId>io.r2dbc</groupId>
            <artifactId>r2dbc-postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        ...
</dependencies>

Нам нужно включить spring-boot-starter-data-r2dbc, чтобы включить spring-data-r2dbc. В этом примере мы будем использовать базу данных postgresql, поэтому нам нужно добавить r2dbc-postgresql, чтобы получить необходимую реализацию драйвера r2dbc.

2.1.2 Конфигурация базы данных

Мы можем либо добавить детали подключения к нашей базе данных в application.properties:

spring.r2dbc.url=r2dbc:postgresql://localhost/studentdb
spring.r2dbc.username=user
spring.r2dbc.password=secret

или используйте конфигурацию на основе Java:

import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactoryOptions;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import static io.r2dbc.spi.ConnectionFactoryOptions.*;

@Configuration
public class R2DBCConfig {

    @Bean
    public ConnectionFactory connectionFactory() {
        return ConnectionFactories.get(
                ConnectionFactoryOptions.builder()
                        .option(DRIVER, "postgresql")
                        .option(HOST, "localhost")
                        .option(USER, "user")
                        .option(PASSWORD, "secret")
                        .option(DATABASE, "studentdb")
                        .build());
    }
}

2.1.3 STUDENTSERVICE

Теперь давайте посмотрим на StudentService, который использует StudentController:

@Service
public class StudentService {

    @Autowired
    private StudentRepository studentRepository;

    public StudentService() {
    }

    public Flux<Student> findStudentsByName(String name) {
        return (name != null) ? studentRepository.findByName(name) : studentRepository.findAll();
    }

    public Mono<Student> findStudentById(long id) {
        return studentRepository.findById(id);
    }

    public Mono<Student> addNewStudent(Student student) {
        return studentRepository.save(student);
    }

    public Mono<Student> updateStudent(long id, Student student) {
        return studentRepository.findById(id)
                .flatMap(s -> {
                    student.setId(s.getId());
                    return studentRepository.save(student);
                });

    }

    public Mono<Void> deleteStudent(Student student) {
        return studentRepository.delete(student);
    }

}

Как видите, он использует StudentRepository для выполнения различных операций с базой данных над учащимися. Итак, теперь давайте взглянем на этот репозиторий.

2.1.4 STUDENTREPOSITORY

StudentRepository - это реализация ReactiveCrudRepository. Это интерфейс из Spring Data R2DBC для общих операций CRUD с использованием типов Project Reactor. Поскольку ReactiveCrudRepository уже содержит определения для большинства методов репозитория, которые мы используем в StudentService (findAll, findById, save и delete), нам нужно объявить следующее:

public interface StudentRepository extends ReactiveCrudRepository<Student, Long> {

    public Flux<Student> findByName(String name);

}

Более сложные запросы также можно определить, добавив аннотацию @Query к методу и указав фактический sql.

Помимо ReactiveCrudRepository, существует также расширение ReactiveSortingRepository, которое предоставляет дополнительные методы для извлечения отсортированных сущностей.

2.1.5 STUDENT

Наконец, давайте посмотрим на реализацию Student:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Table
public class Student {

    @Id
    private Long id;
    private String name;
    private String address;

}

Несколько замечаний:

  • Идентификатор объекта должен быть аннотирован аннотацией Spring Data @Id.

  • Аннотации @Table не обязательны, но ее добавление позволяет сканеру пути к классам находить и предварительно обрабатывать объекты для извлечения связанных метаданных. Если вы не добавите его, это произойдет при первом сохранении объекта, что может немного отрицательно повлиять на производительность.

  • Lombok рекомендуется использовать, чтобы избежать шаблонного кода.

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

2.1.6 Другие варианты запросов

Вместо использования репозитория вы можете выполнить инструкцию SQL напрямую, используя DatabaseClient.

Например, чтобы получить всех студентов:

 public Flux<Student> findAll() {
        DatabaseClient client = DatabaseClient.create(connectionFactory);
        return client.sql("select * from student")
                .map(row -> new Student(row.get("id", Long.class),
                        row.get("name", String.class),
                        row.get("address", String.class))).all();
 }

Также можно использовать R2dbcEntityTemplate для выполнения операций с сущностями. Например:

@Autowired
private R2dbcEntityTemplate template;
public Flux<Student> findAll() {
    return template.select(Student.class).all();
}
public Mono<Void> delete(Student student) {
    return template.delete(student).then();
}

2.2 Другие особенности

2.2.1 Оптимистическая блокировка

Подобно JPA, можно применить аннотацию @Version на уровне поля, чтобы гарантировать, что обновления применяются только к строкам с соответствующей версией - если версия не соответствует, генерируется исключение OptimisticLockingFailureException.

2.2.2 Транзакции

Spring поддерживает управление реактивными транзакциями через SPI ReactiveTransactionManager. Аннотации @Transactional можно наносить на реактивных методах возвращающихся типов Publisher и программное управление транзакциями может быть применено с использованием TransactionalOperator.

2.2.3 Реактивные библиотеки

Как и WebFlux, Spring Data R2DBC требует Project Reactor в качестве основной зависимости, но он совместим с другими реактивными библиотеками, реализующими спецификацию Reactive Streams. Репозитории существуют также для RxJava2 и RxJava3 (см. обзор пакета).

2.2.4 Пул соединений

Для пула соединений доступна библиотека под названием r2dbc-pool. Подробнее о том, как его использовать, читайте здесь.

3. Готовность для продакшн

R2DBC - все еще довольно новая технология. Последние версии релиза на данный момент:

  • Спецификация R2DBC: 0.8.5

  • Spring Data R2DBC: 1.3.1

  • r2dbc-postgresql: 0.8.8

  • r2dbc-pool: 0.8.7

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

4. Подводя итог…

Это сообщение в блоге продемонстрировало, как Spring Data R2DBC можно использовать в приложении WebFlux. Таким образом, мы создали полностью реактивное приложение и подошли к концу этой серии статей о реактивном программировании.

Еще одна интересная инициатива, о которой стоит упомянуть, - это Project Loom. Это проект OpenJDK, который стартовал еще в 2017 году и направлен на обеспечение облегченного параллелизма, включая новый тип потоков Java, которые напрямую не соответствуют выделенным потокам ОС. Такой тип виртуальных потоков было бы намного дешевле создавать и блокировать.

Как вы, возможно, помните из первого сообщения в блоге, ключевыми факторами, лежащими в основе модели реактивного программирования, являются следующие:

  • отход от потока на модель запроса и может обрабатывать больше запросов с небольшим количеством потоков

  • предотвращение блокировки потоков при ожидании завершения операций ввода-вывода

  • упрощение параллельных вызовов

  • поддержка «обратного давления», давая клиенту возможность сообщить серверу, с какой нагрузкой он может справиться

Project Loom кажется очень многообещающим, когда дело доходит до помощи с первыми двумя элементами в этом списке - тогда об этом позаботится сама JVM без какой-либо дополнительной инфраструктуры.

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

Ссылки

R2DBC

Spring Data R2DBC Reference Documentation

r2dbc-postgresql

r2dbc-pool

Project Loom

Going inside Java’s Project Loom and virtual threads

Комментарии (2)


  1. b00
    15.07.2021 22:49

    В примере кода с методом репозитория форматирование поломалось.


    1. val6852 Автор
      19.07.2021 16:34

      Спасибо! Поправил.