Backend Development 9 min read

Mastering Spring Data R2DBC: Reactive Database Access in Spring Boot

This guide explains how to set up Spring Data R2DBC with Spring Boot, covering driver selection, dependency configuration, application properties, entity definitions, CRUD services, controllers, repository usage, and optimistic locking for reactive relational database interactions.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Mastering Spring Data R2DBC: Reactive Database Access in Spring Boot

R2DBC Overview

Spring Data R2DBC is part of the larger Spring Data family and simplifies building repositories based on the Reactive Relational Database Connectivity (R2DBC) specification, which enables reactive, non‑blocking access to SQL databases. It provides a functional approach via R2dbcEntityTemplate as the entry point.

WebFlux Introduction

Spring WebFlux, added in Spring 5, is a fully non‑blocking web framework that runs on Netty, Undertow, or Servlet 3.1+ containers. It coexists with Spring Web MVC, allowing applications to use either or both frameworks.

Dependency Management

<code>&lt;dependencies&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-webflux&lt;/artifactId&gt;
  &lt;/dependency&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-data-r2dbc&lt;/artifactId&gt;
  &lt;/dependency&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;dev.miku&lt;/groupId&gt;
    &lt;artifactId&gt;r2dbc-mysql&lt;/artifactId&gt;
  &lt;/dependency&gt;
&lt;/dependencies&gt;</code>

Application Configuration

<code>spring:
  r2dbc:
    url: r2dbc:mysql://localhost:3306/reactive_db
    username: root
    password: 123123
    pool:
      initialSize: 100
      maxSize: 100
---
logging:
  level:
    org.springframework.r2dbc: DEBUG  # prints executed SQL</code>

Entity and Service CRUD Operations

<code>@Table("T_USERS")
public class Users {
    @Id
    private Long id;
    private String name;
    private String sex;
    private Integer age;
}

@Service
public class UsersService {
    @Resource
    private R2dbcEntityTemplate template;

    @Transactional
    public Mono<Users> insertByTemplate(Users users) {
        return template.insert(users);
    }

    public Mono<Integer> removeByTemplate(Long id) {
        Query query = Query.query(Criteria.where("id").is(id));
        return template.delete(query, Users.class);
    }

    public Mono<Integer> updateByTemplate(Users users) {
        CriteriaDefinition criteria = Criteria.where("id").is(users.getId());
        Query query = Query.query(criteria);
        Update update = Update.update("name", users.getName());
        return template.update(query, update, Users.class);
    }

    public Mono<Users> selectByTemplate(Long id) {
        Query query = Query.query(Criteria.where("id").is(id));
        return template.select(query, Users.class).single();
    }

    public Flux<Users> selectByTemplate(Integer page, Integer size) {
        Query query = Query.empty().offset((page - 1) * size).limit(size);
        return template.select(query, Users.class);
    }

    public Mono<Long> selectByTemplateCount() {
        return template.select(Users.class).count();
    }

    public Mono<ResponseEntity<List<Users>>> selectByTemplatePager(Integer page, Integer size) {
        Mono<List<Users>> datas = this.selectByTemplate(page, size).collectList();
        Mono<Long> count = this.selectByTemplateCount();
        return datas.zipWith(count, (list, c) ->
            ResponseEntity.ok()
                .header("count", c + "")
                .header("page", page + "")
                .header("size", size + "")
                .body(list)
        );
    }
}</code>

Controller Layer

<code>@RestController
public class UsersController {
    @Resource
    private UsersService us;

    @PostMapping("/insert")
    public Mono<Users> insertByTemplate(@RequestBody Users users) {
        return us.insertByTemplate(users);
    }

    @GetMapping("/remove/{id}")
    public Mono<Integer> removeByTemplate(@PathVariable("id") Long id) {
        return us.removeByTemplate(id);
    }

    @PostMapping("/update")
    public Mono<Integer> updateByTemplate(@RequestBody Users users) {
        return us.updateByTemplate(users);
    }

    @GetMapping("/query/{id}")
    public Mono<Users> selectByTemplate(@PathVariable("id") Long id) {
        return us.selectByTemplate(id).single();
    }

    @GetMapping("/pager")
    public Mono<ResponseEntity<List<Users>>> selectByTemplate(Integer page, Integer size) {
        return us.selectByTemplatePager(page, size);
    }

    @GetMapping("/count")
    public Mono<Long> selectByTemplateCount() {
        return us.selectByTemplateCount();
    }
}</code>

R2DBC Repository

Repositories can extend ReactiveCrudRepository or ReactiveSortingRepository . Example methods:

<code>interface ReactivePersonRepository extends ReactiveSortingRepository<Person, String> {
    Mono<Integer> deleteByLastname(String lastname);
    Mono<Void> deletePersonByLastname(String lastname);
    Mono<Boolean> deletePersonByLastname(String lastname);
}</code>

Custom Modifying Query

<code>@Modifying
@Query("UPDATE person SET firstname = :firstname where lastname = :lastname")
Mono<Integer> setFixedFirstnameFor(String firstname, String lastname);
</code>

Optimistic Locking Support

The @Version annotation adds a version column to the entity, ensuring updates succeed only when the version matches, similar to JPA. If a concurrent update changes the version, an OptimisticLockingFailureException is thrown.

<code>@Table
public class Person {
    @Id
    Long id;
    String firstname;
    String lastname;
    @Version
    Long version;
}
</code>

Example that triggers the optimistic‑locking exception:

<code>R2dbcEntityTemplate template = …;
// 1. Insert initial data (version = 0)
Mono<Person> daenerys = template.insert(new Person("Daenerys"));
// 2. Load the inserted row (version = 0)
Person other = template.select(Person.class)
    .matching(query(where("id").is(daenerys.getId()))).first().block();
// 3. Update the first instance (version becomes 1)
daenerys.setLastname("Targaryen");
template.update(daenerys);
// 4. Attempt to update the stale instance – triggers OptimisticLockingFailureException
template.update(other).subscribe();
</code>

For more details on MySQL R2DBC configuration, see the official GitHub repository.

Reactive ProgrammingSpring BootDatabase AccessOptimistic LockingSpring Data R2DBC
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.