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:

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

The complete source code is available here.

Happy learning 🙂

 

 

Share This:

5 thoughts on “Spring Data R2DBC CRUD Example

  1. I am actually pleased to read this website posts which carries tons of helpful data, thanks for providing such information.

  2. Nice post. I learn something totally new and challenging on blogs I stumbleupon on a daily basis. It’s always exciting to read articles from other authors and practice something from their websites.

  3. Hi, To avoid such issues – “Failed to update table [product]. Row with Id [40] does not exist.”, can we not first check if id exists in the DB and then only update it or else throw data not found, record doesn’t exists? What you say?

    1. That is completely different from the problem what we are trying to solve. That is, we need to insert a new record where id is not null. by default r2dbc would not allow.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.