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.repository.CustomerRepository;
 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 CustomerRepository customerRepository;
30    final DataService dataService;
31
32    @GetMapping("/all")
33    public Flux<Customer> findAll() {
34        return customerRepository.findAll();
35    }
36
37    @GetMapping("/{id}")
38    public Mono<ResponseEntity<Customer>> findById(@PathVariable UUID id) {
39        return customerRepository.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 customerRepository.save(customer);
47    }
48
49    @PutMapping(value = "/update")
50    public Mono<Customer> update(@RequestBody Customer customer) {
51        return customerRepository.findById(customer.getId())
52                .flatMap(c -> {
53                    c.setName(customer.getName());
54                    c.setAge(customer.getAge());
55                    c.setPaymentType(customer.getPaymentType());
56                    return Mono.just(c);
57                })
58                .flatMap(p -> customerRepository.save(p));
59    }
60
61    @DeleteMapping(value = "/{id}")
62    public Mono<Void> delete(@PathVariable UUID id) {
63        return customerRepository.deleteById(id);
64    }
65
66    @GetMapping("/find")
67    public Flux<Customer> find(@RequestParam String name, @RequestParam Integer age) {
68        return customerRepository.findByNameAndAge(name, age);
69    }
70
71    @GetMapping("/page")
72    public Mono<Page<Customer>> findPage(@RequestParam("page") int page, @RequestParam("size") int size) {
73        return dataService.getCustomers(PageRequest.of(page, size));
74    }
75
76    @GetMapping("/search")
77    public Flux<Customer> search(Customer customer) {
78        return dataService.search(customer);
79    }
80}
 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    name: test-db
 7  liquibase:
 8    enabled: true
 9    change-log: db/changelog/db.changelog-main.yaml
10    url: jdbc:postgresql://localhost:5432/test-db
11    user: test
12    password: test@123

Setup

Project 65

Spring Webflux & R2DBC

https://gitorko.github.io/spring-webflux-r2dbc/

Version

Check version

1$java --version
2openjdk 17.0.3 2022-04-19 LTS

Postgres DB

1docker run -p 5432:5432 --name pg-container -e POSTGRES_PASSWORD=password -d postgres:9.6.10
2docker ps
3docker exec -it pg-container psql -U postgres -W postgres
4CREATE USER test WITH PASSWORD 'test@123';
5CREATE DATABASE "test-db" WITH OWNER "test" ENCODING UTF8 TEMPLATE template0;
6GRANT ALL PRIVILEGES ON DATABASE "test-db" to test;
7 
8docker stop pg-container
9docker start pg-container

Ensure you login with test user and create the table.

1docker exec -it pg-container psql -U test -W test-db
2\dt

Create the table

1CREATE TABLE customer (
2   id  SERIAL PRIMARY KEY,
3   name VARCHAR(50) NOT NULL,
4   age INT NOT NULL
5);

Dev

To run the code.

1./gradlew clean build
2./gradlew bootRun

Postman

Import the postman collection to postman

Postman Collection

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