Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 50 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
# Reactive ~~JDBC~~ Experiment
# Reactive R2DBC Experiment

This is a simple experiment to test Spring 5's Webflux Module's [Functional Programming Model](https://docs.spring.io/spring/docs/5.0.0.BUILD-SNAPSHOT/spring-framework-reference/html/web-reactive.html#_functional_programming_model) interaction with the Reactiverse [reactive-pg-client](https://reactiverse.io/reactive-pg-client/guide/java/index.html).
This is a simple experiment to test Spring WebFlux [Functional Programming Model](https://docs.spring.io/spring-framework/reference/web/webflux-functional.html) interaction with PostgreSQL using [R2DBC](https://r2dbc.io/).

> Disclaimer: the `reactive-pg-client` does **not** implement the [JDBC](http://download.oracle.com/otn-pub/jcp/jdbc-4_1-mrel-spec/jdbc4.1-fr-spec.pdf?AuthParam=1529679008_7acd6035892acd847bba6ff8dd5242d1) specification.
> Disclaimer: R2DBC does **not** implement the [JDBC](http://download.oracle.com/otn-pub/jcp/jdbc-4_1-mrel-spec/jdbc4.1-fr-spec.pdf?AuthParam=1529679008_7acd6035892acd847bba6ff8dd5242d1) specification - it's a fully reactive alternative.

## Technologies

* Spring Boot 4.0.1
* Spring WebFlux
* Spring Data R2DBC
* Java 21
* Gradle 9.1
* PostgreSQL R2DBC Driver 1.0.7
* Testcontainers 1.21.3
* Lombok 1.18.42

## Prerequisites

* An account with [Space Developer role](https://docs.cloudfoundry.org/concepts/roles.html#roles) access on a Cloud Foundry foundation, e.g., [Pivotal Web Services](https://run.pivotal.io)
* [CF CLI](https://github.com/cloudfoundry/cli#downloads) 6.37.0 or better if you want to push the application to a Cloud Foundry (CF) instance
* [httpie](https://httpie.org/#installation) 0.9.9 or better to simplify interaction with API endpoints
* Java [JDK](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) 1.8u172 or better to compile and run the code
* [Gradle](https://gradle.org/releases/) 4.8 or better to build and package source code
* Java [JDK](https://adoptium.net/) 21 or better to compile and run the code
* [Gradle](https://gradle.org/releases/) 9.1 or better to build and package source code (wrapper included)
* Docker for [Mac](https://store.docker.com/editions/community/docker-ce-desktop-mac) or [Windows](https://store.docker.com/editions/community/docker-ce-desktop-windows) for spinning up a local instance of Postgres and Adminer (a database administration interface)


Expand Down Expand Up @@ -51,30 +62,26 @@ gradle build

Click the `Login` button

3. Click on the `SQL command` link

Link is in the upper left hand-corner of the interface

4. Cut-and-paste the contents of [people.ddl](people.ddl) into the text area, then click the `Execute` button

5. Start the application
3. Start the application

Start a new Terminal session and type

```bash
gradle bootRun
```

6. Let's create some data using the API
> Spring Data R2DBC will automatically initialize the schema from schema.sql if present.

4. Let's create some data using the API

```bash
http POST localhost:8080/person firstName=Dweezil lastName=Zappa age=48

HTTP/1.1 202 Accepted
content-length: 0
Content-Type: application/json
```

7. Verify that we can find the person we added
5. Verify that we can find the person we added

```bash
http localhost:8080/person
Expand All @@ -93,7 +100,7 @@ gradle build
]
```

8. Let's ask for a person by id
6. Let's ask for a person by id

```bash
http localhost:8080/person/582279d1-9bd1-4e49-946c-ac720de0e04f
Expand Down Expand Up @@ -188,22 +195,19 @@ gradle build
> We're interested in `vcap_services.elephantsql.uri`
> The URI consists of {vendor}://{username}:{password}@{server}:5432/{database}

6. We'll set an environment variable
6. Configure R2DBC connection using Cloud Foundry service binding

```bash
cf set-env reactive-jdbc-demo PG_LOOKUP_KEY {service name}
```
> `{service name}` above should match value in steps 3 and 4
Cloud Foundry will automatically configure the R2DBC connection from the bound PostgreSQL service.

7. Now let's startup the application

```bash
cf start reactive-jdbc-demo
```

8. Launch Adminer to administer the database
> Spring Data R2DBC will automatically initialize the schema.

The `people` table doesn't exist yet, so we need to create it
8. (Optional) Launch Adminer to verify database setup

```bash
docker-compose up -d
Expand All @@ -223,11 +227,7 @@ gradle build

Click the `Login` button

9. Click on the `SQL command` link

10. Cut-and-paste the contents of [people.ddl](people.ddl) into the text area, then click the `Execute` button

11. Follow steps 6-8 above in `How to run locally` to interact with API
9. Follow steps 4-6 above in `How to run locally` to interact with API

But replace occurrences of `localhost:8080` with URL to application hosted on Cloud Foundry

Expand Down Expand Up @@ -260,18 +260,30 @@ gradle build
```bash
cf delete reactive-jdbc-demo
```

## What to look forward to?

* Asynchronous Database Access ([ADBA](https://blogs.oracle.com/java/jdbc-next:-a-new-asynchronous-api-for-connecting-to-a-database))
* ADBA over JDBC ([AoJ](https://github.com/oracle/oracle-db-examples/blob/master/java/AoJ/README.md))
## Key Features

### Spring Data R2DBC Benefits

* **Fully Reactive**: Non-blocking database operations from top to bottom
* **Repository Pattern**: Clean, type-safe data access with ReactiveCrudRepository
* **Spring Boot Integration**: Automatic configuration and connection pool management
* **Testcontainers Support**: Easy integration testing with actual PostgreSQL containers
* **Cloud-Native**: Built-in support for Cloud Foundry service bindings

### Migration Notes

Oracle continues to work on ADBA while having released AoJ under an Apache license to get community feedback.
This project has been modernized from the deprecated `reactive-pg-client` to **Spring Data R2DBC**, which provides:

Maybe we will see something concrete in JDK 11?
* Standard Spring Data repository interfaces
* Automatic connection pool management
* Better error handling and transaction support
* Active community and long-term support
* Built-in Spring Boot autoconfiguration

## What else is there to play with?
## Learn More

* [rxjava2-jdbc](https://github.com/davidmoten/rxjava2-jdbc)
* [Vert.x JDBC Client](https://vertx.io/docs/vertx-jdbc-client/java/)
* Reactive Relational Database Connectivity Client ([R2DBC](https://github.com/r2dbc/r2dbc-client))
* [Spring Data R2DBC Reference](https://docs.spring.io/spring-data/r2dbc/reference/)
* [R2DBC Specification](https://r2dbc.io/)
* [PostgreSQL R2DBC Driver](https://github.com/pgjdbc/r2dbc-postgresql)
* [Spring WebFlux Functional Endpoints](https://docs.spring.io/spring-framework/reference/web/webflux-functional.html)
45 changes: 24 additions & 21 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,40 +1,43 @@
buildscript {
ext {
springBootVersion = '4.0.0'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
plugins {
id 'java'
id 'eclipse'
id 'org.springframework.boot' version '4.0.1'
id 'io.spring.dependency-management' version '1.1.7'
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'


group = 'io.pivotal'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 8

java {
sourceCompatibility = JavaVersion.VERSION_25
targetCompatibility = JavaVersion.VERSION_25
}

repositories {
mavenCentral()
}

dependencyManagement {
imports {
mavenBom "tools.jackson:jackson-bom:3.0.2"
}
}

dependencies {
annotationProcessor('org.projectlombok:lombok:1.18.42')
implementation('org.projectlombok:lombok:1.18.42')
implementation('org.springframework.boot:spring-boot-configuration-processor')
implementation('org.springframework.boot:spring-boot-starter-actuator')
implementation('org.springframework.boot:spring-boot-starter-webflux')
implementation('io.reactiverse:reactive-pg-client:0.11.4')
runtime('org.springframework.boot:spring-boot-devtools')
implementation('org.springframework.boot:spring-boot-starter-data-r2dbc')
runtimeOnly('org.postgresql:r2dbc-postgresql')
runtimeOnly('org.springframework.boot:spring-boot-devtools')
testImplementation('org.springframework.boot:spring-boot-starter-test')
testImplementation('io.projectreactor:reactor-test')
testImplementation('ru.yandex.qatools.embed:postgresql-embedded:2.10')

testImplementation('org.testcontainers:postgresql:1.21.3')
testImplementation('org.testcontainers:junit-jupiter:1.21.3')
}

test {
useJUnitPlatform()
}
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
11 changes: 11 additions & 0 deletions src/main/java/io/pivotal/reactive/jdbc/Person.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

import java.util.UUID;

import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand All @@ -10,6 +14,7 @@
import lombok.NoArgsConstructor;
import lombok.ToString;

@Table("people")
@Builder
@Getter
@EqualsAndHashCode
Expand All @@ -18,8 +23,14 @@
@NoArgsConstructor(access=AccessLevel.PACKAGE)
public class Person {

@Id
private UUID id;

@Column("first_name")
private String firstName;

@Column("last_name")
private String lastName;

private Integer age;
}
26 changes: 14 additions & 12 deletions src/main/java/io/pivotal/reactive/jdbc/PersonHandler.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.pivotal.reactive.jdbc;

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;
import static org.springframework.web.reactive.function.BodyInserters.fromValue;

import java.util.UUID;

Expand All @@ -22,28 +22,30 @@ public class PersonHandler {
private final PersonRepository repository;

public Mono<ServerResponse> listPeople(ServerRequest request) {
log.info("Attempting " + request.methodName() + " " + request.path());
log.info("Attempting " + request.method().name() + " " + request.path());
Flux<Person> people = repository.allPeople();
return ServerResponse.ok()
.contentType(APPLICATION_JSON)
.body(people, Person.class);
}

public Mono<ServerResponse> createPerson(ServerRequest request) {
log.info("Attempting " + request.methodName() + " " + request.path());
Mono<Person> person = request.bodyToMono(Person.class);
return ServerResponse.accepted()
.build(repository.savePerson(person));
log.info("Attempting " + request.method().name() + " " + request.path());
return request.bodyToMono(Person.class)
.flatMap(repository::save)
.flatMap(person -> ServerResponse.accepted()
.contentType(APPLICATION_JSON)
.body(fromValue(person)));
}

public Mono<ServerResponse> getPerson(ServerRequest request) {
log.info("Attempting " + request.methodName() + " " + request.path());
log.info("Attempting " + request.method().name() + " " + request.path());
UUID personId = UUID.fromString(request.pathVariable("id"));
Mono<ServerResponse> notFound = ServerResponse.notFound().build();
Mono<Person> personMono = this.repository.getPerson(personId);
return personMono.flatMap(
person -> ServerResponse.ok().contentType(APPLICATION_JSON)
.body(fromObject(person)))
.switchIfEmpty(notFound);
return repository.findById(personId)
.flatMap(person -> ServerResponse.ok()
.contentType(APPLICATION_JSON)
.body(fromValue(person)))
.switchIfEmpty(notFound);
}
}
Loading