From 94919c3ec1f8074f02500c3754889d6b97f20afc Mon Sep 17 00:00:00 2001 From: Thomas Segismont Date: Thu, 21 Mar 2024 15:12:50 +0100 Subject: [PATCH 1/5] Improvements to the Reactive SQL Clients Fixes #39601 Also: - adds a link to the quickstart - informs the about dev services for databases - updates snippets --- .../main/asciidoc/reactive-sql-clients.adoc | 150 +++++++++++------- 1 file changed, 96 insertions(+), 54 deletions(-) diff --git a/docs/src/main/asciidoc/reactive-sql-clients.adoc b/docs/src/main/asciidoc/reactive-sql-clients.adoc index 8cf98e36d58e8..f947e2a4ebf80 100644 --- a/docs/src/main/asciidoc/reactive-sql-clients.adoc +++ b/docs/src/main/asciidoc/reactive-sql-clients.adoc @@ -31,6 +31,8 @@ Feedback is welcome on our https://groups.google.com/d/forum/quarkus-dev[mailing In this guide, you will learn how to implement a simple CRUD application exposing data stored in *PostgreSQL* over a RESTful API. +The code presented is available in this {quickstarts-base-url}[GitHub repository] under the link:{quickstarts-tree-url}/getting-started-reactive-crud[`getting-started-reactive-crud` directory]. + NOTE: Extension and connection pool class names for each client can be found at the bottom of this document. IMPORTANT: If you are not familiar with the Quarkus Vert.x extension, consider reading the xref:vertx.adoc[Using Eclipse Vert.x] guide first. @@ -38,7 +40,17 @@ IMPORTANT: If you are not familiar with the Quarkus Vert.x extension, consider r 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; @@ -46,6 +58,7 @@ public class Fruit { public String name; public Fruit() { + // default constructor. } public Fruit(String name) { @@ -61,7 +74,9 @@ public class Fruit { [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 may also start a database up front: [source,bash] ---- @@ -152,42 +167,79 @@ quarkus.datasource.password=quarkus_test 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 may 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. +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]. 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; + } + + void onStart(@Observes StartupEvent ev) { + if (schemaCreate) { + initdb(); + } } -} -private void initdb() { - // TODO + private void initdb() { + // TODO + } } ---- @@ -201,15 +253,19 @@ It returns a `Uni` and thus can be composed to execute queries sequentially: ---- 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 need to 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. @@ -223,44 +279,26 @@ In development mode, the database is set up with a few rows in the `fruits` tabl 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 = 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` and thus can be converted to a `Multi`: - -[source,java] ----- -Multi 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 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`. +<2> Convert each `io.vertx.mutiny.sqlclient.Row` to a `Fruit`. -[source,java] -.src/main/java/org/acme/vertx/Fruit.java ----- -public static Multi 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. +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 @@ -279,7 +317,7 @@ Lastly, open your browser and navigate to http://localhost:8080/fruits, you shou [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 @@ -387,12 +425,13 @@ With `GET`, `POST` and `DELETE` methods implemented, we may now create a minimal We will use https://jquery.com/[jQuery] to simplify interactions with the backend: [source,html] +./src/main/resources/META-INF/resources/fruits.html ---- - Reactive PostgreSQL Client - Quarkus + Reactive REST - Quarkus @@ -413,6 +452,8 @@ We will use https://jquery.com/[jQuery] to simplify interactions with the backen ---- +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 @@ -420,6 +461,7 @@ In the JavaScript code, we need a function to refresh the list of fruits when: * a fruit is deleted. [source,javascript] +./src/main/resources/META-INF/resources/fruits.js ---- function refresh() { $.get('/fruits', function (fruits) { From e1c5764defbbe627f165e118e522ccc7410fb7ae Mon Sep 17 00:00:00 2001 From: Thomas Segismont Date: Thu, 21 Mar 2024 15:18:11 +0100 Subject: [PATCH 2/5] Replace may with might/can --- docs/src/main/asciidoc/reactive-sql-clients.adoc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/main/asciidoc/reactive-sql-clients.adoc b/docs/src/main/asciidoc/reactive-sql-clients.adoc index f947e2a4ebf80..3d6b6a2cfdc14 100644 --- a/docs/src/main/asciidoc/reactive-sql-clients.adoc +++ b/docs/src/main/asciidoc/reactive-sql-clients.adoc @@ -76,7 +76,7 @@ public class Fruit { ==== 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 may also start a database up front: +You might also start a database up front: [source,bash] ---- @@ -167,7 +167,7 @@ quarkus.datasource.password=quarkus_test 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 @@ -243,7 +243,7 @@ public class DBInit { } ---- -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. @@ -421,7 +421,7 @@ public Uni delete(Long id) { } ---- -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] @@ -786,7 +786,7 @@ quarkus.datasource.reactive.max-lifetime=PT60M 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. +You might need 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: From e7caef2bc4ec85f313dd45362199b04f0366aac3 Mon Sep 17 00:00:00 2001 From: Thomas Segismont Date: Thu, 21 Mar 2024 15:45:07 +0100 Subject: [PATCH 3/5] Replace need to with must/have to --- docs/src/main/asciidoc/reactive-sql-clients.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/main/asciidoc/reactive-sql-clients.adoc b/docs/src/main/asciidoc/reactive-sql-clients.adoc index 3d6b6a2cfdc14..bd0f88b32a0f6 100644 --- a/docs/src/main/asciidoc/reactive-sql-clients.adoc +++ b/docs/src/main/asciidoc/reactive-sql-clients.adoc @@ -130,7 +130,7 @@ If you are not familiar with Mutiny, check xref:mutiny-primer.adoc[Mutiny - an i === 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[] @@ -202,7 +202,7 @@ public class FruitResource { == Database schema and seed data -Before we implement the REST endpoint and data management code, we need to set up the database schema. +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]. @@ -262,7 +262,7 @@ client.query("DROP TABLE IF EXISTS fruits").execute() [NOTE] ==== -Wondering why we need to block until the latest query is completed? +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. ==== @@ -786,7 +786,7 @@ quarkus.datasource.reactive.max-lifetime=PT60M Sometimes, the database connection pool cannot be configured only by declaration. -You might 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: From f273312f5055ab054e6962cd0e716e141bfd0fe3 Mon Sep 17 00:00:00 2001 From: Thomas Segismont Date: Fri, 22 Mar 2024 16:22:55 +0100 Subject: [PATCH 4/5] Follow structure recommendations --- docs/src/main/asciidoc/reactive-sql-clients.adoc | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/src/main/asciidoc/reactive-sql-clients.adoc b/docs/src/main/asciidoc/reactive-sql-clients.adoc index bd0f88b32a0f6..be59c4ecdd687 100644 --- a/docs/src/main/asciidoc/reactive-sql-clients.adoc +++ b/docs/src/main/asciidoc/reactive-sql-clients.adoc @@ -31,8 +31,6 @@ Feedback is welcome on our https://groups.google.com/d/forum/quarkus-dev[mailing In this guide, you will learn how to implement a simple CRUD application exposing data stored in *PostgreSQL* over a RESTful API. -The code presented is available in this {quickstarts-base-url}[GitHub repository] under the link:{quickstarts-tree-url}/getting-started-reactive-crud[`getting-started-reactive-crud` directory]. - NOTE: Extension and connection pool class names for each client can be found at the bottom of this document. IMPORTANT: If you are not familiar with the Quarkus Vert.x extension, consider reading the xref:vertx.adoc[Using Eclipse Vert.x] guide first. @@ -72,6 +70,11 @@ public class Fruit { } ---- +== Prerequisites + +{prerequisites-docker} +include::{includes}/prerequisites.adoc[] + [TIP] ==== 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. @@ -84,6 +87,15 @@ docker run -it --rm=true --name quarkus_test -e POSTGRES_USER=quarkus_test -e PO ---- ==== +== Solution + +We recommend that you follow the instructions in the next sections and create the application step by step. +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 From ab9831f292ac5770f4d094c58b5b7f037da38cbc Mon Sep 17 00:00:00 2001 From: Thomas Segismont Date: Sat, 23 Mar 2024 10:32:35 +0100 Subject: [PATCH 5/5] Fix prerequisites-docker attribute assignment --- docs/src/main/asciidoc/reactive-sql-clients.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/reactive-sql-clients.adoc b/docs/src/main/asciidoc/reactive-sql-clients.adoc index be59c4ecdd687..5b64977d6a91c 100644 --- a/docs/src/main/asciidoc/reactive-sql-clients.adoc +++ b/docs/src/main/asciidoc/reactive-sql-clients.adoc @@ -72,7 +72,7 @@ public class Fruit { == Prerequisites -{prerequisites-docker} +:prerequisites-docker: include::{includes}/prerequisites.adoc[] [TIP]