Skip to main content

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

spring-webflux-postgresql-crud-2-proj-struct.png

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:

spring-webflux-postgresql-crud-1-create-product.png

Analogously:

  • Read Operation,
  • Update Operation,
  • Delete Operation.

Source code:

https://github.com/ZbCiok/zjc-examples/tree/main/spring/webflux/spring-webflux-postgresql-crud-02