In this article, I would like to show you how to perform various CRUD operations using Spring Data R2DBC along with Spring WebFlux.
Spring Data R2DBC:
R2DBC — Reactive Relational DB connectivity.
JPA is an old specification for blocking drivers. R2DBC is a new specification for reactive/non-blocking drivers for relational DBs.
Unlike JPA, the new specification does NOT support @OneToMany/@ManyToMany etc. So, do not expect all the features of JPA/Hibernate as R2DBC is completely different.
Sample Application:
We‘ll develop a Spring WebFlux application (product-service) which is responsible for creating new products / retrieving all products / delete or update an existing product to perform various CRUD operations using R2DBC.
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>r2dbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>r2dbc</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
product.java
import lombok.Data;
import lombok.ToString;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
@Data
@ToString
@Table
public class Product {
@Id
private Integer id;
private String description;
private Double price;
}
DB Setup.
CREATE TABLE product(
id serial PRIMARY KEY,
description VARCHAR (500),
price numeric (10, 2) NOT NULL
);
ProductRepository.java
import com.vinsguru.reactive.r2dbc.entity.Product;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ProductRepository extends ReactiveCrudRepository<Product, Integer> {
}
ProductService.java
import com.vinsguru.reactive.r2dbc.entity.Product;
import com.vinsguru.reactive.r2dbc.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class ProductService {
@Autowired
private ProductRepository repository;
public Flux<Product> getAllProducts(){
return this.repository.findAll();
}
public Mono<Product> getProductById(int productId){
return this.repository.findById(productId);
}
public Mono<Product> createProduct(final Product product){
return this.repository.save(product);
}
public Mono<Product> updateProduct(int productId, final Mono<Product> productMono){
return this.repository.findById(productId)
.flatMap(p -> productMono.map(u -> {
p.setDescription(u.getDescription());
p.setPrice(u.getPrice());
return p;
}))
.flatMap(p -> this.repository.save(p));
}
public Mono<Void> deleteProduct(final int id){
return this.repository.deleteById(id);
}
}
R2DBC.java
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactoryOptions;
import org.springframework.context.annotation.Bean;
import static io.r2dbc.pool.PoolingConnectionFactoryProvider.MAX_SIZE;
import static io.r2dbc.spi.ConnectionFactoryOptions.*;
//use either application.properties or this way by creating ConnectionFactory
//@Configuration
public class R2DBCConfig {
@Bean
public ConnectionFactory connectionFactory() {
return ConnectionFactories.get(
ConnectionFactoryOptions.builder()
.option(DRIVER, "postgresql")
.option(HOST, "localhost")
.option(PORT, 5432)
.option(USER, "postgres")
.option(PASSWORD, "postgres")
.option(DATABASE, "postgres")
.option(MAX_SIZE, 40)
.build());
}
}
ProductController.java
import com.vinsguru.reactive.r2dbc.entity.Product;
import com.vinsguru.reactive.r2dbc.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("product")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("all")
public Flux<Product> getAll(){
return this.productService.getAllProducts();
}
@GetMapping("{productId}")
public Mono<ResponseEntity<Product>> getProductById(@PathVariable int productId){
return this.productService.getProductById(productId)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PostMapping
public Mono<Product> createProduct(@RequestBody Mono<Product> productMono){
return productMono.flatMap(this.productService::createProduct);
}
@PutMapping("{productId}")
public Mono<Product> updateProduct(@PathVariable int productId,
@RequestBody Mono<Product> productMono){
return this.productService.updateProduct(productId, productMono);
}
@DeleteMapping("{id}")
public Mono<Void> deleteProduct(@PathVariable int id){
return this.productService.deleteProduct(id);
}
}
application.properties
spring.r2dbc.url=r2dbc:postgresql://localhost:5432/postgres
spring.r2dbc.username=postgres
spring.r2dbc.password=postgres
MainApp.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class R2dbcApplication {
public static void main(String[] args) {
SpringApplication.run(R2dbcApplication.class, args);
}
}
Curl — POST
curl -X POST \
http://localhost:8080/product \
-H 'cache-control: no-cache' \
-H 'content-type: application/json' \
-H 'postman-token: a983975e-ccf8-d582-23f6-bae537c24aa6' \
-d '{
"description" : "Apple 1",
"price" : 1000
}'
GET
curl -X GET \
http://localhost:8080/product/all
PUT and Delete
curl -X DELETE \
http://localhost:8080/product/3