r2dbc

Spring Data R2DBC Entity Callback

Overview:

In this tutorial, I would like to show you how we could use Spring Data R2DBC Entity Callback to register a set of hooks which allows us to invoke certain methods behind the scenes when working with Entity objects.

I assume you are already familiar with R2DBC. If not, check the below article for more information.

R2DBC Entity Callback:

Spring Data provides some convenient hooks / methods to be executed to check and modify entity objects before saving or after retrieving! This hooks are also included as part of R2DBC. It is called R2DBC Entity Callback.

We have following hooks.

  • BeforeConvert
  • AfterConvert
  • BeforeSave
  • AfterSave
Hook Description
BeforeConvert To modify an entity object before it is converted into OutboundRow object.
Use this to modify Entity object before saving.
BeforeSave Entity object is converted into OutboundRow. We can still modify the domain object.
Use this to modify OutboundRow before saving.
AfterSave Entity object is saved at this point. If it was a new entity, ID would have been updated.
Use this to modify Entity object after saving.
AfterConvert Entity object is retrieved from DB.
Use this to modify Entity object after reading from DB.

These hooks/callbacks can be used in your application in cases like audit logging support / raise an event for an event-driven application etc.

For ex: repository.save method might invoke these hooks in this order.

r2dbc entity callback

Sample Application:

Lets consider a simple application, product-service, which handles product specific information. We have some requirements like

  • Product description should not contain any special chars. Remove if they are present before saving.
  • Seasonal special discounts should be applied when we retrieve product information from DB.

Lets see how we could achieve these using these hooks.

  • BeforeConvert
  • AfterConvert

Project Setup:

Lets create a Spring Boot application with these dependencies.

Product Table:

I have the table structure as shown here.

create table product (
    id bigint auto_increment,
    description varchar(50),
    price decimal,
    primary key (id)
);

Entity:

@Data
@ToString
@NoArgsConstructor
public class Product {

    @Id
    private Integer id;
    private String description;
    private Double price;

    public Product(String description, Double price) {
        this.description = description;
        this.price = price;
    }
}

Product Repository:

@Repository
public interface ProductRepository extends ReactiveCrudRepository<Product, Integer> {
}

Inserting New Product:

I create a simple command line runner which inserts product like this.

@Autowired
private ProductRepository repository;


// initialize product

String desc = "new- -pro/d&&u#c%t";
Product product = new Product(desc, 10.0);        

// insert

this.repository.save(product)
              .subscribe(p -> System.out.println("After inserting : " + p));

It produces below output.

After inserting : Product(id=1, description=new- -pro/d&&u#c%t, price=10.0)

R2DBC Entity Callback – BeforeConvert:

Lets implement the requirement for removing any chars which are not allowed to be in the product description. Allowed chars are just alphabets and a space. Here we could use BeforeConvertCallback hook to modify the entity object just before saving.

@Component
public class ProductBeforeConvert implements BeforeConvertCallback<Product> {

    private static final String PATTERN = "[^a-zA-Z ]";

    @Override
    public Publisher<Product> onBeforeConvert(Product product, SqlIdentifier sqlIdentifier) {
        var updatedDescription = product.getDescription().replaceAll(PATTERN, "");
        System.out.println("Actual : " + product.getDescription());
        System.out.println("Updated : " + updatedDescription);
        product.setDescription(updatedDescription);
        return Mono.just(product);
    }

}

Once we create the about Spring Component, lets re-run the exact same command line runner we had created earlier. It produces below output.

Actual : new- -pro/d&&u#c%t
Updated : new product
After inserting : Product(id=1, description=new product, price=10.0)

R2DBC Entity Callback – AfterConvert:

Lets implement the other requirement – to apply seasonal global discounts to all products when we select records from DB. Here we do not want to touch the database. We still want to keep the original price as it is. We just want to update the price once it is retrieved from DB.

AfterConvertCallback hook would be a good choice here.

@Component
public class ProductAfterConvert implements AfterConvertCallback<Product> {

    private final double seasonalDiscount = 0.2d;

    @Override
    public Publisher<Product> onAfterConvert(Product product, SqlIdentifier sqlIdentifier) {
        double price = (product.getPrice() * (1-seasonalDiscount));
        System.out.println("Actual  : " + product.getPrice());
        System.out.println("Updated : " + price);
        product.setPrice(price);
        return Mono.just(product);
    }

}

Once the above hook is created, simply try to invoke below method from the product repository.

// get all products
this.repository.findAll()
              .subscribe(System.out::println);

We see the price as 8.0 even though we have the price as 10.0 in the DB.

Actual  : 10.0
Updated : 8.0
Product(id=1, description=new product, price=8.0)

Multiple Callbacks:

We can have multiple implementations for the same hook. For ex: We can have multiple implementations for BeforeConvertCallback. In this case our implementation should implement Ordered as well as shown here to return the order in which it should be executed.

@Component
public class ProductBeforeConvert2 implements BeforeConvertCallback<Product>, Ordered {
    
    @Override
    public int getOrder() {
       // int HIGHEST_PRECEDENCE = -2147483648;
       // int LOWEST_PRECEDENCE = 2147483647;
        return 0;
    }

    @Override
    public Publisher<Product> onBeforeConvert(Product product, SqlIdentifier sqlIdentifier) {
        // your business rule
        return Mono.just(product);
    }
    
}

R2DBC Entity Callback – BeforeSave:

Sometimes the actual table might have more columns and entity object might not contain all the fields. For ex: fields like created_by, created_date etc. But we might want to update these fields. In this case, BeforeConvertCallback will not help much! But we could achieve with BeforeSaveCallback.

I add new column in the table – created_by. Product entity does not have this field.

create table product (
    id bigint auto_increment,
    description varchar(50),
    price decimal,
    created_by varchar(50),
    primary key (id)
);

I could populate the field using BeforeSaveCallback as shown here. [Do note that OutboundRow has already been created at this point. Modifying product entity will NOT have any effect. So We need to update OutboundRow if any]

@Component
public class ProductBeforeSave implements BeforeSaveCallback<Product> {

    @Override
    public Publisher<Product> onBeforeSave(Product product, OutboundRow outboundRow, SqlIdentifier sqlIdentifier) {
        outboundRow.put(SqlIdentifier.unquoted("created_by"), Parameter.from("vinsguru"));
        return Mono.just(product);
    }

}

With this hook, we could update additional fields if any or we could use this hook for audit logging purposes.

Summary:

We were able successfully demonstrate R2DBC Hooks / R2DBC Entity Callbacks.

Learn more about Spring Boot.

The source code is available here.

Happy learning 🙂

Share This:

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.