Google Cloud SQL for PostgreSQL + Spring Boot + Spring WebFlux + R2DBC - Building Reactive REST CRUD APIs
In this section, we will learn how to use Spring Boot, Spring WebFlux, and R2DBC to create a reactive web service that interacts with Google Cloud SQL for PostgreSQL.
Reactive APIs are non-blocking and tend to be more efficient because they’re not tying up processing while waiting for stuff to happen. Reactive systems adopt asynchronous I/O. Reactive apps allow us to scale better if we are dealing with lots of streaming data.
If we are going to build a reactive app, we need it to be reactive all the way down to your database.
Spring WebFlux uses a library called Reactor for its reactive support. The Reactor is an implementation of the Reactive Streams specification. The Reactor Provides two main types called Flux and Mono. Both of these types implement the Publisher interface provided by Reactive Streams. Flux is used to represent a stream of 0..N elements and Mono is used to represent a stream of 0..1 element.
1. A little bit of Background
Google Cloud SQL for PostgreSQL
Spring WebFlux
Spring Boot
2. Create a GCP Project
First, Sign into the Google console at https://console.cloud.google.com.
You can create a new project by first selecting the project dropdown in the top left and selecting "New Project".
Then Click on the "CREATE" button.
Copy "Project ID" and keep it for future purposes.
3. Create a Cloud SQL for PostgreSQL instance.
From cloud console, search for "SQL" like below and click on "SQL" button.
Next, click on "CREATE INSTANCE" button,
Then, click on "Choose PostgreSQL" button,
After that, enter "Instance Id", "Password", "Choose a configuration to start with", "Region" etc...
Note "Password" and keep it safely for future purposes.
Then click on "CREATE INSTANCE" button,
Next, copy "Connection name" and keep it for future purposes.
4. Create a Database.
Click on "CREATE DATABASE" button,
Enter "Database name" and then click on "CREATE" button.
Copy "Database name" and keep it for future purposes.
5. Enable SQL admin API
From cloud console, search for "sqladmin API" like below and click on "sqladmin API" button.
Next, click on "ENABLE" button,
6. Create a service account key
First choose "IAM & Admin" and then click on "Service accounts".
After that, click on "CREATE SERVICE ACCOUNT".
Then, enter service account details like "Service account name", and "Service account ID" and click on "CREATE AND CONTINUE".
Then, grant basic role Editor.
Finally click on "DONE" button.
Then, from "Actions" click on "Manage keys".
Then, click on "Create new key".
Then, choose "Key type" as JSON and click on "CREATE" button.
Service account keys in JSON format will be download. Keep the file safe for future purposes.
7. Creating a simple spring boot web application
First, open the Spring initializr https://start.spring.io/
Then, Provide the Group and Artifact name. We have provided Group name com.knf.dev.demo and Artifact spring-boot-webflux-gcp-sql-postgres-r2dbc-example. Here I selected the Maven project - language Java 17 - Spring Boot 3.0.4 and add Spring Reactive Web, and GCP Support.
Then, click on the Generate button. When we click on the Generate button, it starts packing the project in a .zip(spring-boot-webflux-gcp-sql-postgres-r2dbc-example) file and downloads the project. Then, Extract the Zip file.
Then, import the project on your favourite IDE.
Final Project directory:
In the pom.xml, add CloudSQL Starter for PostgreSQL R2DBC:
<!-- Add CloudSQL Starter for PostgreSQL R2DBC -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>spring-cloud-gcp-starter-sql-postgres-r2dbc</artifactId>
</dependency>
<!-- Add CloudSQL Starter for PostgreSQL R2DBC -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>spring-cloud-gcp-starter-sql-postgres-r2dbc</artifactId>
</dependency>
Complete 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.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.knf.dev.demo</groupId>
<artifactId>spring-boot-webflux-gcp-sql-postgres-r2dbc-example</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-webflux-gcp-sql-postgres-r2dbc-example</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
<spring-cloud-gcp.version>4.1.2</spring-cloud-gcp.version>
<spring-cloud.version>2022.0.1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>spring-cloud-gcp-starter</artifactId>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>spring-cloud-gcp-starter-sql-postgres-r2dbc</artifactId>
</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>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>spring-cloud-gcp-dependencies</artifactId>
<version>${spring-cloud-gcp.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
Add the following configuration to application.properties file:
spring.cloud.gcp.sql.database-name=studentdb
spring.cloud.gcp.sql.instance-connection-name=knf-gcp-demo-project-123:us-central1:knf-demo-instance
spring.cloud.gcp.project-id=knf-gcp-demo-project-123
spring.cloud.gcp.credentials.location=classpath:knf-gcp-demo-project-123-9bf47abde11f.json
# Uncomment if root password is specified
spring.r2dbc.password=YourPassword
# Leave empty to use the default username (`postgres`), uncomment and fill out if you specified a user
# spring.r2dbc.username=
Note: In this example, we placed service account key JSON file in resources folder (Not recommended in production environment).
More secure way is place JSON in somewhere in server or docker image, then create environment variable "GOOGLE_APPLICATION_CREDENTIALS" and give the location to your JSON FILE.
If your application is running on Google App Engine or Google Compute Engine, in most cases you should omit the "spring.cloud.gcp.credentials.location" property and instead, let Spring Cloud GCP Core Starter find the correct credentials for those environments.
schema.sql
CREATE TABLE IF NOT EXISTS students(
id serial primary key,
name VARCHAR(40) not null,
email VARCHAR(40) not null
);
Create DBConfig.java
package com.knf.dev.demo.config;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer;
import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator;
@Configuration
public class DBConfig {
@Bean
ConnectionFactoryInitializer initializer(
ConnectionFactory connectionFactory) {
ConnectionFactoryInitializer initializer =
new ConnectionFactoryInitializer();
initializer.setConnectionFactory(connectionFactory);
initializer.setDatabasePopulator(
new ResourceDatabasePopulator(
new ClassPathResource("schema.sql")));
return initializer;
}
}
Spring Data R2dbc does not maintain the database schemas, so you have to do it yourself. Spring Data R2dbc provides a ConnectionFactoryInitializer to allow you execute sql scripts on database when it is connected.
Create model Student.java
package com.knf.dev.demo.model;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
@Table("students")
public class Student {
@Id
private Long id;
private String name;
private String email;
public Student() {
}
public Student(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
Create Reactive Crud Repository [StudentRepository.java]
package com.knf.dev.demo.repository;
import com.knf.dev.demo.model.Student;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
public interface StudentRepository
extends ReactiveCrudRepository<Student, Long> {
}
ReactiveCrudRepository: Interface for generic CRUD operations on a repository for a specific type. This repository follows reactive paradigms and uses Project Reactor types which are built on top of Reactive Streams.
Create Student Controller
package com.knf.dev.demo.controller;
import com.knf.dev.demo.model.Student;
import com.knf.dev.demo.repository.StudentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/v1/")
public class StudentController {
@Autowired
private StudentRepository studentRepository;
@GetMapping("students")
Flux<Student> getAll() {
return studentRepository.findAll();
}
@GetMapping("students/{id}")
Mono<Student> getStudent(@PathVariable("id") Long id) {
return studentRepository.findById(id);
}
@PostMapping("students")
Mono<Student> addStudent(@RequestBody Student student) {
return studentRepository.save(student);
}
@PutMapping("students/{id}")
private Mono<Student> updateStudent(@PathVariable("id") Long id,
@RequestBody Student student) {
return studentRepository.findById(id).flatMap(user1 -> {
student.setId(id);
return studentRepository.save(student);
}).switchIfEmpty(Mono.empty());
}
@DeleteMapping("students/{id}")
Mono<Void> deleteById(@PathVariable("id") Long id) {
return studentRepository.findById(id).flatMap(p ->
studentRepository.deleteById(p.getId()));
}
}
- Spring @RestController annotation is used to create RESTful web services using Spring MVC. Spring RestController takes care of mapping request data to the defined request handler method. Once response body is generated from the handler method, it converts it to JSON response.
- @RequestMapping is used to map web requests onto specific handler classes and/or handler methods. @RequestMapping can be applied to the controller class as well as methods.
- We can use the @Autowired to mark a dependency which Spring is going to resolve and inject.
- @GetMapping annotation for mapping HTTP GET requests onto specific handler methods.
- @PostMapping annotation for mapping HTTP POST requests onto specific handler methods.
- @PutMapping annotation for mapping HTTP PUT requests onto specific handler methods.
- @DeleteMapping annotation for mapping HTTP DELETE requests onto specific handler methods.
- @RequestBody annotation is used to indicating a method parameter should be bind to the body of the HTTP request. Internally, this annotation uses HTTP Message converters to convert the body of HTTP requests to domain objects.
- @PathVariable annotation used on a method argument to bind it to the value of a URI template variable.
- Spring WebFlux uses a library called Reactor for its reactive support. The Reactor is an implementation of the Reactive Streams specification. The Reactor Provides two main types called Flux and Mono. Both of these types implement the Publisher interface provided by Reactive Streams. Flux is used to represent a stream of 0..N elements and Mono is used to represent a stream of 0..1 element.
Run the application - Application.java
package com.knf.dev.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Application is the entry point that sets up the Spring Boot application. The @SpringBootApplication annotation enables auto-configuration and component scanning.
Step1: Download or clone the source code from GitHub to a local machine - Click here!
Step 2: mvn clean install
Step 3: Run the Spring Boot application - mvn spring-boot:run
OR
Run this Spring boot application from
- IntelliJ IDEA IDE by right click - Run 'Application.main()'
- Eclipse/STS - You can right click the project or the Application.java file and run as java application or Spring boot application.
Step1: Download or clone the source code from GitHub to a local machine - Click here!
Step 2: mvn clean install
Step 3: Run the Spring Boot application - mvn spring-boot:run
OR
Run this Spring boot application from
- IntelliJ IDEA IDE by right click - Run 'Application.main()'
- Eclipse/STS - You can right click the project or the Application.java file and run as java application or Spring boot application.