postgresql-crud-02
Description
We’re gonna build a Spring Boot WebFlux Rest API example – CRUD application that uses Spring Data Reactive (R2DBC) to interact with embedded database.
Requirements
- Java 17+
- Spring Boot 3.0.2
- PostgreSQL
- Maven 3.6+
Database
docker run -it --rm \
-p 5432:5432 \
-e POSTGRES_USER=dbuser \
-e POSTGRES_PASSWORD=theSecretPassword \
-e POSTGRES_DB=micronaut \
postgres:11.5-alpine
Project Structure

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.2</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>zjc.examples</groupId>
<artifactId>spring-webflux-postgresql-crud-02</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-webflux-postgresql-crud-02</name>
<description>spring-webflux-postgresql-crud-02</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>r2dbc-postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
spring.r2dbc.url=r2dbc:postgresql://localhost:5432/micronaut
spring.r2dbc.username=dbuser
spring.r2dbc.password=theSecretPassword
schema.sql
CREATE TABLE IF NOT EXISTS tutorial (id SERIAL PRIMARY KEY, title VARCHAR(255), description VARCHAR(255), published BOOLEAN);
Enable r2dbc Repositories
@EnableR2dbcRepositories is a Spring annotation that is used to enable R2DBC repositories in a Spring Boot application. It provides a convenient way to create a repository layer in a Spring Boot application that uses R2DBC to interact with a database.
Because R2DBC repository support is enabled in our Spring Boot application by default (spring.data.r2dbc.repositories.enabled=true), so that the @EnableR2dbcRepositories is not necessary.
The @EnableR2dbcRepositories annotation could be added to a configuration class in your application, typically the main class that is annotated with @SpringBootApplication:
SpringBootR2dbcPostgresqlExampleApplication.java
package zjc.examples.spring.r2dbc.postgresql;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
//import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer;
import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator;
import org.springframework.web.reactive.config.EnableWebFlux;
import io.r2dbc.spi.ConnectionFactory;
@EnableWebFlux
//@EnableR2dbcRepositories
@SpringBootApplication
public class SpringBootR2dbcPostgresqlExampleApplication {
@Bean
ConnectionFactoryInitializer initializer(ConnectionFactory connectionFactory) {
ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer();
initializer.setConnectionFactory(connectionFactory);
initializer.setDatabasePopulator(new ResourceDatabasePopulator(new ClassPathResource("schema.sql")));
return initializer;
}
public static void main(String[] args) {
SpringApplication.run(SpringBootR2dbcPostgresqlExampleApplication.class, args);
}
}
TutorialRepository.java
package zjc.examples.spring.r2dbc.postgresql.repository;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import zjc.examples.spring.r2dbc.postgresql.model.Tutorial;
import reactor.core.publisher.Flux;
@Repository
public interface TutorialRepository extends R2dbcRepository<Tutorial, Integer>{
Flux<Tutorial> findByTitleContaining(String title);
Flux<Tutorial> findByPublished(boolean isPublished);
}
Tutorial.java (Data Model)
package zjc.examples.spring.r2dbc.postgresql.model;
import org.springframework.data.annotation.Id;
public class Tutorial {
@Id
private int id;
private String title;
private String description;
private boolean published;
public Tutorial() {
}
public Tutorial(String title, String description, boolean published) {
this.title = title;
this.description = description;
this.published = published;
}
public void setId(int id) {
this.id = id;
}
public long getId() {
return id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean getPublished() {
return published;
}
public void setPublished(boolean isPublished) {
this.published = isPublished;
}
@Override
public String toString() {
return "Tutorial [id=" + id + ", title=" + title + ", desc=" + description + ", published=" + published + "]";
}
}
TutorialService.java
package zjc.examples.spring.r2dbc.postgresql.service;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import zjc.examples.spring.r2dbc.postgresql.model.Tutorial;
import zjc.examples.spring.r2dbc.postgresql.repository.TutorialRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class TutorialService {
@Autowired
TutorialRepository tutorialRepository;
public Flux<Tutorial> findAll() {
return tutorialRepository.findAll();
}
public Flux<Tutorial> findByTitleContaining(String title) {
return tutorialRepository.findByTitleContaining(title);
}
public Mono<Tutorial> findById(int id) {
return tutorialRepository.findById(id);
}
public Mono<Tutorial> save(Tutorial tutorial) {
return tutorialRepository.save(tutorial);
}
public Mono<Tutorial> update(int id, Tutorial tutorial) {
return tutorialRepository.findById(id).map(Optional::of).defaultIfEmpty(Optional.empty())
.flatMap(optionalTutorial -> {
if (optionalTutorial.isPresent()) {
tutorial.setId(id);
return tutorialRepository.save(tutorial);
}
return Mono.empty();
});
}
public Mono<Void> deleteById(int id) {
return tutorialRepository.deleteById(id);
}
public Mono<Void> deleteAll() {
return tutorialRepository.deleteAll();
}
public Flux<Tutorial> findByPublished(boolean isPublished) {
return tutorialRepository.findByPublished(isPublished);
}
}
TutorialController.java (Spring WebFlux Rest API Controller)
package zjc.examples.spring.r2dbc.postgresql.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import zjc.examples.spring.r2dbc.postgresql.model.Tutorial;
import zjc.examples.spring.r2dbc.postgresql.service.TutorialService;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@CrossOrigin(origins = "http://localhost:8081")
@RestController
@RequestMapping("/api")
public class TutorialController {
@Autowired
TutorialService tutorialService;
@GetMapping("/tutorials")
@ResponseStatus(HttpStatus.OK)
public Flux<Tutorial> getAllTutorials(@RequestParam(required = false) String title) {
if (title == null)
return tutorialService.findAll();
else
return tutorialService.findByTitleContaining(title);
}
@GetMapping("/tutorials/{id}")
@ResponseStatus(HttpStatus.OK)
public Mono<Tutorial> getTutorialById(@PathVariable("id") int id) {
return tutorialService.findById(id);
}
@PostMapping("/tutorials")
@ResponseStatus(HttpStatus.CREATED)
public Mono<Tutorial> createTutorial(@RequestBody Tutorial tutorial) {
return tutorialService.save(new Tutorial(tutorial.getTitle(), tutorial.getDescription(), false));
}
@PutMapping("/tutorials/{id}")
@ResponseStatus(HttpStatus.OK)
public Mono<Tutorial> updateTutorial(@PathVariable("id") int id, @RequestBody Tutorial tutorial) {
return tutorialService.update(id, tutorial);
}
@DeleteMapping("/tutorials/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public Mono<Void> deleteTutorial(@PathVariable("id") int id) {
return tutorialService.deleteById(id);
}
@DeleteMapping("/tutorials")
@ResponseStatus(HttpStatus.NO_CONTENT)
public Mono<Void> deleteAllTutorials() {
return tutorialService.deleteAll();
}
@GetMapping("/tutorials/published")
@ResponseStatus(HttpStatus.OK)
public Flux<Tutorial> findByPublished() {
return tutorialService.findByPublished(true);
}
}
Running
Run Spring Boot application: mvn spring-boot:run
Postman. Create Tutorials:

Analogously:
- Read Operation,
- Update Operation,
- Delete Operation.
Source code:
https://github.com/ZbCiok/zjc-examples/tree/main/spring/webflux/spring-webflux-postgresql-crud-02