Site icon Vinsguru

Spring Data R2DBC CRUD Example

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:

CREATE TABLE product(
   id serial PRIMARY KEY,
   description VARCHAR (500),
   price numeric (10, 2) NOT NULL
);
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.

@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.

spring.r2dbc.url=r2dbc:postgresql://localhost:5432/productdb
spring.r2dbc.username=admin
spring.r2dbc.password=admin
@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.

The complete source code is available here.

Happy learning 🙂

 

 

Share This:

Exit mobile version