Реактивное программирование со Spring, часть 4 R2DBC

Это четвертая часть серии заметок о реактивном программировании, в которой будет представлено введение в 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> { <span class="token keyword" style="box-sizing: border-box; margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; color: rgb(0, 119, 170);">public</span> Flux<span class="token operator" style="box-sizing: border-box; margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; color: rgb(166, 127, 89); background: rgba(255, 255, 255, 0.5);">&lt;</span>Student<span class="token operator" style="box-sizing: border-box; margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; color: rgb(166, 127, 89); background: rgba(255, 255, 255, 0.5);">&gt;</span> <span class="token function" style="box-sizing: border-box; margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline;">findByName<span class="token punctuation" style="box-sizing: border-box; margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; color: rgb(153, 153, 153);">(</span></span>String name<span class="token punctuation" style="box-sizing: border-box; margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; color: rgb(153, 153, 153);">)</span><span class="token punctuation" style="box-sizing: border-box; margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; color: rgb(153, 153, 153);">;</span>  }

Более сложные запросы также можно определить, добавив аннотацию @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

ссылка на оригинал статьи https://habr.com/ru/post/565060/

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *