Overview:
In this article, I would like to show you how to perform various CRUD operations using Spring Data R2DBC along with Spring WebFlux. I would use Postgres in this example. If you are using H2/MySQL..etc the concept remains same.
Spring Data R2DBC:
R2DBC stands for Reactive Relational DB connectivity.
As a Java developer, you know what JPA (Java Persistence API) is already. 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 are going to develop a Spring WebFlux application called 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.
Project Setup:
Go to spring initializer and create a Spring Boot project with below dependencies.
Database Setup:
- I create a file called init.sql to create this table.
CREATE TABLE product(
id serial PRIMARY KEY,
description VARCHAR (500),
price numeric (10, 2) NOT NULL
);
- I use Postgres DB with the help of docker-compose as shown here. When the container starts, it would use the init.sql to set up the table.
version: "3"
services:
postgres:
image: postgres
container_name: postgres
environment:
- POSTGRES_USER=admin
- POSTGRES_PASSWORD=admin
- POSTGRES_DB=productdb
volumes:
- ./data/db:/var/lib/postgresql/data
- ./data/init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- 5432:5432
pgadmin:
image: dpage/pgadmin4
container_name: pgadmin
environment:
- PGADMIN_DEFAULT_EMAIL=admin@admin.com
- PGADMIN_DEFAULT_PASSWORD=admin
ports:
- 80:80
Entity:
Our entity class to represent the product is as shown here.
- Do note that we can not add @Entity here. As we had said earlier, this is not JPA. But there is @Table from Spring. It is optional.
@Data
@ToString
@Table // optional
public class Product {
@Id
private Integer id;
private String description;
private Double price;
}
Spring Data R2DBC – Reactive Repository:
Spring Data does all the heavy lifting as usual. We need to create a repository for our entity class by extending ReactiveCrudRepository.
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ProductRepository extends ReactiveCrudRepository<Product, Integer> {
}
Spring Data R2DBC CRUD:
Let’s create a service class to perform the CRUD operations via Spring Data Reactive Repository.
@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);
}
}
REST API:
Now it is time for us to expose the service via REST API.
@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("{productId}")
public Mono<Void> deleteProduct(@PathVariable int productId){
return this.productService.deleteProduct(productId);
}
}
Configuration:
The Spring Data reactive driver requires a configuration like this to connect to the Postgres DB. You can follow either of these approaches to connect to the DB.
- Approach 1: using application.properties
spring.r2dbc.url=r2dbc:postgresql://localhost:5432/productdb
spring.r2dbc.username=admin
spring.r2dbc.password=admin
- Approach 2: Exposing connection factory bean.
@Configuration
public class R2DBCConfig {
@Bean
public ConnectionFactory connectionFactory() {
return ConnectionFactories.get(
ConnectionFactoryOptions.builder()
.option(DRIVER, "postgresql")
.option(HOST, "localhost")
.option(PORT, 5432)
.option(USER, "vinsguru")
.option(PASSWORD, "admin")
.option(DATABASE, "productdb")
.option(MAX_SIZE, 40)
.build());
}
}
Spring Data R2DBC CRUD – Demo:
Now at this point, We should be able to fetch all products / create / update / delete products.
For ex: Sending a POST request with below request body /product endpoint creates a new record in the DB.
{
"description": "Sony 4k TV",
"price": 300.95
}
Upsert:
Here we clearly assume that for any new product, id would be null to successfully save into our database. What if we provide an id which does not exist!!? If you want to store the data as new record with the given id, we can try as shown here.
@Transactional
public Mono<Product> updateProduct(final Product product){
return this.repository.findById(product.getId())
.flatMap(p -> {
p.setDescription(product.getDescription());
p.setPrice(product.getPrice());
return this.repository.save(p);
}).switchIfEmpty(this.repository.save(product)); // save if the product with id is not present
}
For example, I send the below request to the /product endpoint. (the id 40 is not present).
{
"id": 40,
"description": "IPhone",
"price": 500.95
}
I get the below exception.
org.springframework.dao.TransientDataAccessResourceException: Failed to update table [product]. Row with Id [40] does not exist.
at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.lambda$update$5(R2dbcEntityTemplate.java:402) ~[spring-data-r2dbc-1.1.0.RC1.jar:1.1.0.RC1]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ Handler com.vinsguru.reactive.r2dbc.controller.ProductController#upsertProduct(Product) [DispatcherHandler]
|_ checkpoint ⇢ HTTP POST "/product/save" [ExceptionHandlingWebHandler]
This is because we are trying to save a new product. The id field should be null. If it is present, Spring expects the given id to be present in the DB. So we can not insert a new record with the given id. But We can fix this by implementing the Persistable interface. If the isNew method returns new, R2DBC inserts the record with the given id.
@Data
@ToString
public class Product implements Persistable<Integer> {
@Id
private Integer id;
private String description;
private Double price;
@Transient
private boolean newProduct;
@Override
@Transient
public boolean isNew() {
return this.newProduct || id == null;
}
public Product setAsNew(){
this.newProduct = true;
return this;
}
}
The service class method is modified as shown here.
@Transactional
public Mono<Product> updateProduct(final Product product){
return this.repository.findById(product.getId())
.flatMap(p -> {
p.setDescription(product.getDescription());
p.setPrice(product.getPrice());
return this.repository.save(p);
}).switchIfEmpty(this.repository.save(product.setAsNew())); // save if the product with id is not present
}
Summary:
We were able to successfully demonstrate Spring Data R2DBC CRUD operations with Postgres.
Read more about R2DBC & WebFlux.
- Spring Data R2DBC Pagination
- Spring Data R2DBC Entity Callback
- Server Sent Events With Spring WebFlux
The complete source code is available here.
Happy learning 🙂