Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements to the Reactive SQL Clients guide #39620

Merged
merged 5 commits into from
Mar 23, 2024
Merged
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
172 changes: 113 additions & 59 deletions docs/src/main/asciidoc/reactive-sql-clients.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,25 @@
The application shall manage fruit entities:

[source,java]
.src/main/java/org/acme/reactive/crud/Fruit.java
----
package org.acme.reactive.crud;

import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import io.vertx.mutiny.pgclient.PgPool;
import io.vertx.mutiny.sqlclient.Row;
import io.vertx.mutiny.sqlclient.RowSet;
import io.vertx.mutiny.sqlclient.Tuple;

public class Fruit {

public Long id;

public String name;

public Fruit() {
// default constructor.
}

public Fruit(String name) {
Expand All @@ -59,19 +70,35 @@
}
----

== Prerequisites

:prerequisites-docker:
include::{includes}/prerequisites.adoc[]

[TIP]
====
Do you need a ready-to-use PostgreSQL server to try out the examples?
If you start the application in dev mode, Quarkus provides you with a https://quarkus.io/guides/databases-dev-services[zero-config database] out of the box.

You might also start a database up front:

[source,bash]
----
docker run -it --rm=true --name quarkus_test -e POSTGRES_USER=quarkus_test -e POSTGRES_PASSWORD=quarkus_test -e POSTGRES_DB=quarkus_test -p 5432:5432 postgres:14.1
----
====

== Solution

We recommend that you follow the instructions in the next sections and create the application step by step.

Check warning on line 92 in docs/src/main/asciidoc/reactive-sql-clients.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Fluff] Depending on the context, consider using 'using more direct instructions' rather than 'recommend'. Raw Output: {"message": "[Quarkus.Fluff] Depending on the context, consider using 'using more direct instructions' rather than 'recommend'.", "location": {"path": "docs/src/main/asciidoc/reactive-sql-clients.adoc", "range": {"start": {"line": 92, "column": 4}}}, "severity": "INFO"}
However, you can go right to the completed example.

Clone the Git repository: `git clone {quickstarts-clone-url}`, or download an {quickstarts-archive-url}[archive].

The solution is located in the `getting-started-reactive-crud` link:{quickstarts-tree-url}/getting-started-reactive-crud[directory].

== Installing

=== Reactive PostgreSQL Client extension

Check warning on line 101 in docs/src/main/asciidoc/reactive-sql-clients.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in 'Reactive PostgreSQL Client extension'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'Reactive PostgreSQL Client extension'.", "location": {"path": "docs/src/main/asciidoc/reactive-sql-clients.adoc", "range": {"start": {"line": 101, "column": 5}}}, "severity": "INFO"}

First, make sure your project has the `quarkus-reactive-pg-client` extension enabled.
If you are creating a new project, use the following command:
Expand Down Expand Up @@ -115,7 +142,7 @@
=== JSON Binding

We will expose `Fruit` instances over HTTP in the JSON format.
Consequently, you also need to add the `quarkus-rest-jackson` extension:
Consequently, you must also add the `quarkus-rest-jackson` extension:

:add-extension-extensions: rest-jackson
include::{includes}/devtools/extension-add.adoc[]
Expand Down Expand Up @@ -152,46 +179,83 @@
quarkus.datasource.reactive.url=postgresql://localhost:5432/quarkus_test
----

With that you may create your `FruitResource` skeleton and `@Inject` a `io.vertx.mutiny.pgclient.PgPool` instance:
With that you can create your `FruitResource` skeleton and inject a `io.vertx.mutiny.pgclient.PgPool` instance:

[source,java]
.src/main/java/org/acme/vertx/FruitResource.java
----
package org.acme.reactive.crud;

import java.net.URI;

import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.ResponseBuilder;
import jakarta.ws.rs.core.Response.Status;

import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import io.vertx.mutiny.pgclient.PgPool;

@Path("fruits")
public class FruitResource {

@Inject
io.vertx.mutiny.pgclient.PgPool client;
private final PgPool client;

public FruitResource(PgPool client) {
this.client = client;
}
}
----

== Database schema and seed data

Before we implement the REST endpoint and data management code, we need to set up the database schema.
It would also be convenient to have some data inserted up-front.
Before we implement the REST endpoint and data management code, we must set up the database schema.
It would also be convenient to have some data inserted up front.

For production, we would recommend to use something like the xref:flyway.adoc[Flyway database migration tool].

Check warning on line 220 in docs/src/main/asciidoc/reactive-sql-clients.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Fluff] Depending on the context, consider using 'using more direct instructions' rather than 'recommend'. Raw Output: {"message": "[Quarkus.Fluff] Depending on the context, consider using 'using more direct instructions' rather than 'recommend'.", "location": {"path": "docs/src/main/asciidoc/reactive-sql-clients.adoc", "range": {"start": {"line": 220, "column": 4}}}, "severity": "INFO"}
But for development we can simply drop and create the tables on startup, and then insert a few fruits.

[source,java]
.src/main/java/org/acme/vertx/FruitResource.java
./src/main/java/org/acme/reactive/crud/DBInit.java
----
@Inject
@ConfigProperty(name = "myapp.schema.create", defaultValue = "true") // <1>
boolean schemaCreate;
package org.acme.reactive.crud;

import io.quarkus.runtime.StartupEvent;
import io.vertx.mutiny.pgclient.PgPool;
import org.eclipse.microprofile.config.inject.ConfigProperty;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;

void config(@Observes StartupEvent ev) {
if (schemaCreate) {
initdb();
@ApplicationScoped
public class DBInit {

private final PgPool client;
private final boolean schemaCreate;

public DBInit(PgPool client, @ConfigProperty(name = "myapp.schema.create", defaultValue = "true") boolean schemaCreate) {
this.client = client;
this.schemaCreate = schemaCreate;
}
}

private void initdb() {
// TODO
void onStart(@Observes StartupEvent ev) {
if (schemaCreate) {
initdb();
}
}

private void initdb() {
// TODO
}
}
----

TIP: You may override the default value of the `myapp.schema.create` property in the `application.properties` file.
TIP: You might override the default value of the `myapp.schema.create` property in the `application.properties` file.

Almost ready!
To initialize the DB in development mode, we will use the client simple `query` method.
Expand All @@ -201,15 +265,19 @@
----
client.query("DROP TABLE IF EXISTS fruits").execute()
.flatMap(r -> client.query("CREATE TABLE fruits (id SERIAL PRIMARY KEY, name TEXT NOT NULL)").execute())
.flatMap(r -> client.query("INSERT INTO fruits (name) VALUES ('Orange')").execute())
.flatMap(r -> client.query("INSERT INTO fruits (name) VALUES ('Pear')").execute())
.flatMap(r -> client.query("INSERT INTO fruits (name) VALUES ('Apple')").execute())
.flatMap(r -> client.query("INSERT INTO fruits (name) VALUES ('Kiwi')").execute())
.flatMap(r -> client.query("INSERT INTO fruits (name) VALUES ('Durian')").execute())
.flatMap(r -> client.query("INSERT INTO fruits (name) VALUES ('Pomelo')").execute())
.flatMap(r -> client.query("INSERT INTO fruits (name) VALUES ('Lychee')").execute())
.await().indefinitely();
----

NOTE: Wondering why we need to block until the latest query is completed?
This code is part of a `@PostConstruct` method and Quarkus invokes it synchronously.
[NOTE]
====
Wondering why we must block until the latest query is completed?
This code is part of a method that `@Observes` the `StartupEvent` and Quarkus invokes it synchronously.
As a consequence, returning prematurely could lead to serving requests while the database is not ready yet.
====

That's it!
So far we have seen how to configure a pooled client and execute simple queries.
Expand All @@ -223,44 +291,26 @@
To retrieve all the data, we will use the `query` method again:

[source,java]
./src/main/java/org/acme/reactive/crud/Fruit.java
----
Uni<RowSet<Row>> rowSet = client.query("SELECT id, name FROM fruits ORDER BY name ASC").execute();
----

When the operation completes, we will get a `RowSet` that has all the rows buffered in memory.
A `RowSet` is an `java.lang.Iterable<Row>` and thus can be converted to a `Multi`:

[source,java]
----
Multi<Fruit> fruits = rowSet
.onItem().transformToMulti(set -> Multi.createFrom().iterable(set))
.onItem().transform(Fruit::from);
----

The `Fruit#from` method converts a `Row` instance to a `Fruit` instance.
It is extracted as a convenience for the implementation of the other data management methods:
public static Multi<Fruit> findAll(PgPool client) {
return client.query("SELECT id, name FROM fruits ORDER BY name ASC").execute()
.onItem().transformToMulti(set -> Multi.createFrom().iterable(set)) // <1>
.onItem().transform(Fruit::from); // <2>
}

[source,java]
.src/main/java/org/acme/vertx/Fruit.java
----
private static Fruit from(Row row) {
return new Fruit(row.getLong("id"), row.getString("name"));
}
private static Fruit from(Row row) {
return new Fruit(row.getLong("id"), row.getString("name"));
}
----

Putting it all together, the `Fruit.findAll` method looks like:
<1> Transform the `io.vertx.mutiny.sqlclient.RowSet` to a `Multi<Row>`.
<2> Convert each `io.vertx.mutiny.sqlclient.Row` to a `Fruit`.

[source,java]
.src/main/java/org/acme/vertx/Fruit.java
----
public static Multi<Fruit> findAll(PgPool client) {
return client.query("SELECT id, name FROM fruits ORDER BY name ASC").execute()
.onItem().transformToMulti(set -> Multi.createFrom().iterable(set))
.onItem().transform(Fruit::from);
}
----
The `Fruit#from` method converts a `Row` instance to a `Fruit` instance.

Check warning on line 310 in docs/src/main/asciidoc/reactive-sql-clients.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/reactive-sql-clients.adoc", "range": {"start": {"line": 310, "column": 68}}}, "severity": "INFO"}
It is extracted as a convenience for the implementation of the other data management methods.

And the endpoint to get all fruits from the backend:
Then, add the endpoint to get all fruits from the backend:

[source,java]
.src/main/java/org/acme/vertx/FruitResource.java
Expand All @@ -279,7 +329,7 @@

[source,json]
----
[{"id":3,"name":"Apple"},{"id":1,"name":"Orange"},{"id":2,"name":"Pear"}]
[{"id":2,"name":"Durian"},{"id":1,"name":"Kiwi"},{"id":4,"name":"Lychee"},{"id":3,"name":"Pomelo"}]
----

=== Prepared queries
Expand Down Expand Up @@ -383,16 +433,17 @@
}
----

With `GET`, `POST` and `DELETE` methods implemented, we may now create a minimal web page to try the RESTful application out.
With `GET`, `POST` and `DELETE` methods implemented, we can now create a minimal web page to try the RESTful application out.
We will use https://jquery.com/[jQuery] to simplify interactions with the backend:

[source,html]
./src/main/resources/META-INF/resources/fruits.html
----
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Reactive PostgreSQL Client - Quarkus</title>
<title>Reactive REST - Quarkus</title>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
<script type="application/javascript" src="fruits.js"></script>
Expand All @@ -413,13 +464,16 @@
</html>
----

TIP: Quarkus automatically serves static resources located under the `META-INF/resources` directory.

In the JavaScript code, we need a function to refresh the list of fruits when:

* the page is loaded, or
* a fruit is added, or
* a fruit is deleted.

[source,javascript]
./src/main/resources/META-INF/resources/fruits.js
----
function refresh() {
$.get('/fruits', function (fruits) {
Expand Down Expand Up @@ -744,9 +798,9 @@

Sometimes, the database connection pool cannot be configured only by declaration.

You may need to read a specific file only present in production, or retrieve configuration data from a proprietary configuration server.
For example, you might have to read a specific file only present in production, or retrieve configuration data from a proprietary configuration server.

In this case, you can customize pool creation by creating a class implementing an interface which depends on the target database:

Check warning on line 803 in docs/src/main/asciidoc/reactive-sql-clients.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using ', which (non restrictive clause preceded by a comma)' or 'that (restrictive clause without a comma)' rather than 'which'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using ', which (non restrictive clause preceded by a comma)' or 'that (restrictive clause without a comma)' rather than 'which'.", "location": {"path": "docs/src/main/asciidoc/reactive-sql-clients.adoc", "range": {"start": {"line": 803, "column": 92}}}, "severity": "INFO"}

[cols="30,70"]
|===
Expand Down
Loading