Spring Webflux & R2DBC

Overview

Webflux integration with reactive R2DBC with liquibase. R2DBC stands for Reactive Relational Database Connectivity, It provides a reactive driver to connect to relational database.

Github: https://github.com/gitorko/project65

Spring Data R2DBC

Spring Data R2DBC aims at being conceptually easy. In order to achieve this it does NOT offer caching, lazy loading, write behind or many other features of ORM frameworks. This makes Spring Data R2DBC a simple, limited, opinionated object mapper.

The following databases are supported via r2dbc libraries

  1. H2 (io.r2dbc:r2dbc-h2)
  2. MariaDB (org.mariadb:r2dbc-mariadb)
  3. Microsoft SQL Server (io.r2dbc:r2dbc-mssql)
  4. MySQL (dev.miku:r2dbc-mysql)
  5. jasync-sql MySQL (com.github.jasync-sql:jasync-r2dbc-mysql)
  6. Postgres (io.r2dbc:r2dbc-postgresql)
  7. Oracle (com.oracle.database.r2dbc:oracle-r2dbc)

Code

 1package com.demo.project65;
 2
 3import lombok.extern.slf4j.Slf4j;
 4import org.springframework.boot.SpringApplication;
 5import org.springframework.boot.autoconfigure.SpringBootApplication;
 6
 7@SpringBootApplication
 8@Slf4j
 9public class Main {
10
11    public static void main(String[] args) {
12        SpringApplication.run(Main.class, args);
13    }
14
15}
16
 1package com.demo.project65.controller;
 2
 3import java.util.UUID;
 4
 5import com.demo.project65.domain.Customer;
 6import com.demo.project65.service.CustomerService;
 7import com.demo.project65.service.DataService;
 8import lombok.AllArgsConstructor;
 9import org.springframework.data.domain.Page;
10import org.springframework.data.domain.PageRequest;
11import org.springframework.http.ResponseEntity;
12import org.springframework.web.bind.annotation.DeleteMapping;
13import org.springframework.web.bind.annotation.GetMapping;
14import org.springframework.web.bind.annotation.PathVariable;
15import org.springframework.web.bind.annotation.PostMapping;
16import org.springframework.web.bind.annotation.PutMapping;
17import org.springframework.web.bind.annotation.RequestBody;
18import org.springframework.web.bind.annotation.RequestMapping;
19import org.springframework.web.bind.annotation.RequestParam;
20import org.springframework.web.bind.annotation.RestController;
21import reactor.core.publisher.Flux;
22import reactor.core.publisher.Mono;
23
24@RestController
25@AllArgsConstructor
26@RequestMapping("/customer")
27public class HomeController {
28
29    final CustomerService customerService;
30    final DataService dataService;
31
32    @GetMapping("/all")
33    public Flux<Customer> findAll() {
34        return customerService.findAll();
35    }
36
37    @GetMapping("/{id}")
38    public Mono<ResponseEntity<Customer>> findById(@PathVariable UUID id) {
39        return customerService.findById(id)
40                .map(ResponseEntity::ok)
41                .defaultIfEmpty(ResponseEntity.notFound().build());
42    }
43
44    @PostMapping(value = "/save")
45    public Mono<Customer> save(@RequestBody Customer customer) {
46        return customerService.save(customer);
47    }
48
49    @PutMapping(value = "/update")
50    public Mono<Customer> update(@RequestBody Customer customer) {
51        return customerService.update(customer);
52    }
53
54    @DeleteMapping(value = "/{id}")
55    public Mono<Void> delete(@PathVariable UUID id) {
56        return customerService.deleteById(id);
57    }
58
59    @GetMapping("/find")
60    public Flux<Customer> find(@RequestParam String name, @RequestParam Integer age) {
61        return customerService.findByNameAndAge(name, age);
62    }
63
64    @GetMapping("/page")
65    public Mono<Page<Customer>> findPage(@RequestParam("page") int page, @RequestParam("size") int size) {
66        return customerService.findAllByPage(PageRequest.of(page, size));
67    }
68
69    @PostMapping("/search")
70    public Flux<Customer> search(@RequestBody Customer customer) {
71        return customerService.search(customer);
72    }
73
74    @PostMapping("/findOne")
75    public Mono<Customer> findOne(@RequestBody Customer customer) {
76        return customerService.findOne(customer);
77    }
78}
 1package com.demo.project65.config;
 2
 3import org.springframework.context.annotation.Bean;
 4import org.springframework.context.annotation.Configuration;
 5import org.springframework.data.domain.ReactiveAuditorAware;
 6import org.springframework.data.r2dbc.config.EnableR2dbcAuditing;
 7import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
 8import reactor.core.publisher.Mono;
 9
10@Configuration
11@EnableR2dbcAuditing
12@EnableR2dbcRepositories
13public class DbConfig {
14
15    @Bean
16    public ReactiveAuditorAware<String> auditorAware() {
17        return () -> Mono.just("admin");
18    }
19}
 1package com.demo.project65.repository;
 2
 3import java.util.UUID;
 4
 5import com.demo.project65.domain.Customer;
 6import org.springframework.data.domain.Pageable;
 7import org.springframework.data.r2dbc.repository.Query;
 8import org.springframework.data.r2dbc.repository.R2dbcRepository;
 9import reactor.core.publisher.Flux;
10
11public interface CustomerRepository extends R2dbcRepository<Customer, UUID> {
12
13    @Query("select * from customer e where e.name = $1 and e.age = $2")
14    Flux<Customer> findByNameAndAge(String name, Integer age);
15
16    Flux<Customer> findAllBy(Pageable pageable);
17
18}
 1spring:
 2  r2dbc:
 3    url: r2dbc:postgresql://localhost:5432/test-db
 4    username: test
 5    password: test@123
 6    pool:
 7      enabled: true
 8      initial-size: 10
 9      max-size: 30
10  liquibase:
11    enabled: true
12    change-log: db/changelog/db.changelog-main.yaml
13    url: jdbc:postgresql://localhost:5432/test-db
14    user: test
15    password: test@123
16
17logging:
18  level:
19    org.springframework.r2dbc: DEBUG

Postman

Import the postman collection to postman

Postman Collection

Setup

 1# Project 65
 2
 3Spring Webflux & R2DBC
 4
 5[https://gitorko.github.io/spring-webflux-r2dbc/](https://gitorko.github.io/spring-webflux-r2dbc/)
 6
 7### Version
 8
 9Check version
10
11```bash
12$java --version
13openjdk version "21.0.3" 2024-04-16 LTS
14```
15
16### Postgres DB
17
18```bash
19docker run -p 5432:5432 --name pg-container -e POSTGRES_PASSWORD=password -d postgres:14
20docker ps
21docker exec -it pg-container psql -U postgres -W postgres
22CREATE USER test WITH PASSWORD 'test@123';
23CREATE DATABASE "test-db" WITH OWNER "test" ENCODING UTF8 TEMPLATE template0;
24GRANT ALL PRIVILEGES ON DATABASE "test-db" to test;
25 
26docker stop pg-container
27docker start pg-container
28```
29
30Ensure you login with test user and create the table.
31
32```bash
33docker exec -it pg-container psql -U test -W test-db
34\dt
35```
36
37Create the table
38
39```sql
40CREATE TABLE customer (
41   id  SERIAL PRIMARY KEY,
42   name VARCHAR(50) NOT NULL,
43   age INT NOT NULL
44);
45```
46
47### Dev
48
49To run the code.
50
51```bash
52./gradlew clean build
53./gradlew bootRun
54```

Errors

If you encounter any of the error mentioned below it could probably be because the data type in postgres cant be mapped by r2dbc. Eg: CHAR is not supported, changing to VARCHAR will fix the issue.

1org.springframework.data.mapping.MappingException: Could not read property public java.lang.String com.demo.project65.Customer.name from result set!
2org.springframework.data.r2dbc.function.convert.EntityRowMapper.readFrom(EntityRowMapper.java:103) ~[spring-data-r2dbc-1.0.0.M1.jar:1.0.0.M1]
3Caused by: java.lang.IllegalArgumentException: Cannot decode value of type java.lang.Object
4org.springframework.data.r2dbc.function.convert.EntityRowMapper.readFrom(EntityRowMapper.java:99) ~[spring-data-r2dbc-1.0.0.M1.jar:1.0.0.M1]

If you encounter unit test failures because of r2dbc repository in @SpringBootTest then exclude the classes

1@EnableAutoConfiguration(exclude = {R2dbcAutoConfiguration.class, LiquibaseAutoConfiguration.class})

References

https://spring.io/projects/spring-data-r2dbc

comments powered by Disqus