Это четвертая часть серии заметок о реактивном программировании, в которой будет представлено введение в 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, но двоичные файлы раннего доступа доступны для загрузки.
Ссылки
b00
В примере кода с методом репозитория форматирование поломалось.
val6852 Автор
Спасибо! Поправил.