Helidon SE DB Client Guide

This guide describes the features of Helidon’s DB Client and how to create a sample Helidon SE project that can be used to run some basic examples using the Helidon DB Client.

What You Need

For this 15 minute tutorial, you will need the following:

A Helidon SE ApplicationYou can use your own application or use the Helidon SE Quickstart to create a sample application.
Java SE 17 (Open JDK 17)Helidon requires Java 17+.
Maven 3.6.1+Helidon requires Maven 3.6.1+.
Docker 18.09+You need Docker if you want to build and deploy Docker containers.
Kubectl 1.16.5+If you want to deploy to Kubernetes, you need kubectl and a Kubernetes cluster (you can install one on your desktop.
Verify Prerequisites
java -version
mvn --version
docker --version
kubectl version
Copied
Setting JAVA_HOME
# On Mac
export JAVA_HOME=`/usr/libexec/java_home -v 17`

# On Linux
# Use the appropriate path to your JDK
export JAVA_HOME=/usr/lib/jvm/jdk-17
Copied

Introduction

The Helidon DB Client simplifies how you work with databases in reactive applications. It provides a unified, reactive API for working with databases in a non-blocking way.

Main Features

The main features of Helidon DB Client are:

  • Unified API for data access and query: The API was implemented as a layer above JDBC or MongoDB Reactive Streams Java Driver, so any relational databases with JDBC driver or MongoDB are supported.

  • Reactive database access with non-reactive drivers: Most JDBC drivers are blocking. Using them in a reactive application is problematic. Helidon DB Client allows the use of blocking JDBC drivers in your reactive application by wrapping a blocking driver in an executor service.

  • Observability: Support for health checks, metrics and tracing.

  • Backpressure: Performs database operations only when it is requested by the consumer. This is propagated all the way to the TCP layer.

  • Portability between relational database drivers: Works with native database statements that can be used inline in the code or defined as named statements in database configuration. By moving the native query code to configuration files, the Helidon DB Client allows you to switch to another database by changing the configuration files, not the code.

Getting Started with Helidon DB Client

This section describes how to configure and use the key features of the Helidon DB Client.

Set Up the H2 database

H2 is a Java SQL database that is lightweight and easy to use. If H2 is not installed on your machine, here are few steps to quickly download and set it up:

From Docker

Create a new file in helidon-quickstart-se named Dockerfile.h2. It will be used to create the H2 docker image to run H2 in a container.

Write the following content into the new file created
FROM openjdk:11-jre-slim

ENV H2_VERSION "1.4.199"

ADD "https://repo1.maven.org/maven2/com/h2database/h2/${H2_VERSION}/h2-${H2_VERSION}.jar" /opt/h2.jar

COPY h2.server.properties /root/.h2.server.properties

EXPOSE 8082
EXPOSE 9092

CMD java \
       -cp /opt/h2.jar \
       org.h2.tools.Server \
       -web -webDaemon -webAllowOthers -webPort 8082 \
       -tcp -tcpAllowOthers -tcpPort 9092 \
       -ifNotExists
Copied

Create a new file h2.server.properties in the current directory.

Copy the properties into the properties file.
webSSL=false
webAllowOthers=true
webPort=8082
0=Generic H2 (Server)|org.h2.Driver|jdbc\:h2\:tcp\://localhost\:9092/~/test|sa
Copied
Build the H2 docker image
docker build -f Dockerfile.h2 . -t h2db
Copied
Run the H2 docker image
docker run --rm -p 8082:8082 -p 9092:9092 --name=h2 h2db
Copied

From the Command Line

  1. Download the latest H2 version from the official website: https://www.h2database.com/html/main.html
    Note: Windows operating system users can download the Windows Installer.
  2. Unzip the downloaded file into your directory.
    • Only the h2-{latest-version}.jar, located in the h2/bin folder, will be needed.

  3. Open a terminal window and run the following command to start H2:
Replace {latest-version} with your current H2 version:
java -jar h2-\{latest-version}.jar -webAllowOthers -tcpAllowOthers
Copied
Terminal output
Web Console server running at http://127.0.1.1:8082 (others can connect)
Opening in existing browser session.
TCP server running at tcp://127.0.1.1:9092 (others can connect)
PG server running at pg://127.0.1.1:5435 (only local connections)
Copied

Connect to the Database

Open the console at http://127.0.1.1:8082 in your favorite browser. It displays a login window. Select Generic H2 from Saved Settings. The following settings should be set by default:

  • Driver Class: org.h2.Driver

  • JDBC URL: jdbc:h2:tcp://localhost:9092/~/test

  • User Name: sa

  • Password:

Password must stay empty. Click Connect, the browser displays a web page. The database is correctly set and running.

Create a Sample SE Project Using Maven Archetype

Generate the project sources using the Helidon SE Maven archetype. The result is a simple project that can be used for the examples in this guide.

Run the Maven archetype:
mvn -U archetype:generate -DinteractiveMode=false \
    -DarchetypeGroupId=io.helidon.archetypes \
    -DarchetypeArtifactId=helidon-quickstart-se \
    -DarchetypeVersion=3.2.16 \
    -DgroupId=io.helidon.examples \
    -DartifactId=helidon-quickstart-se \
    -Dpackage=io.helidon.examples.quickstart.se
Copied

A new directory named helidon-quickstart-se is created.

Enter into this directory:
cd helidon-quickstart-se
Copied

Add Dependencies

Navigate to the helidon-quickstart-se directory and open the pom.xml file to add the following Helidon dependencies required to use the DB Client:

Copy these dependencies to pom.xml:
<dependencies>
    <dependency>
        <groupId>io.helidon.dbclient</groupId>       
        <artifactId>helidon-dbclient</artifactId>
    </dependency>
    <dependency>
        <groupId>io.helidon.dbclient</groupId>      
        <artifactId>helidon-dbclient-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>io.helidon.integrations.db</groupId>   
        <artifactId>h2</artifactId>
    </dependency>
    <dependency>
        <groupId>io.helidon.dbclient</groupId>      
        <artifactId>helidon-dbclient-health</artifactId>
    </dependency>
    <dependency>
        <groupId>io.helidon.dbclient</groupId>      
        <artifactId>helidon-dbclient-metrics</artifactId>
    </dependency>
    <dependency>
        <groupId>io.helidon.dbclient</groupId>      
        <artifactId>helidon-dbclient-jsonp</artifactId>
    </dependency>
</dependencies>
Copied
  • DB Client API dependency.
  • Using JDBC driver for this example.
  • H2 driver dependency.
  • Support for health check.
  • Support for metrics.
  • Support for Jsonp.

Configure the DB Client

To configure the application, Helidon uses the application.yaml. The DB Client configuration can be joined in the same file and is located here: src/main/resources.

Copy these properties into application.yaml
db:
  source: jdbc  
  connection:   
    url: "jdbc:h2:tcp://localhost:9092/~/test"
    username: "sa"
    password:
  statements:   
    create-table: "CREATE TABLE IF NOT EXISTS LIBRARY (NAME VARCHAR NOT NULL, INFO VARCHAR NOT NULL)"
    insert-book: "INSERT INTO LIBRARY (NAME, INFO) VALUES (:name, :info)"
    select-book: "SELECT INFO FROM LIBRARY WHERE NAME = ?"
    delete-book: "DELETE FROM LIBRARY WHERE NAME = ?"
Copied
  • Source property support two values: jdbc and mongo.
  • Connection detail we used to set up H2.
  • SQL statements to manage the database.

Build and Set Up Helidon DB Client

In the application Main.class, an instance of DbClient is created based on the configuration from application.yaml.

Create a DbClient in the Main.startServer method:
import io.helidon.dbclient.metrics.DbClientMetrics; 
import io.helidon.dbclient.DbClient;

Config config = Config.create(); // Landmark to add DB client

DbClient dbClient = DbClient.builder()
                .config(config.get("db"))       
                .addService(DbClientMetrics.counter().statementNames("select-book"))   
                .build();
Copied
  • Add import statements
  • Configure the DB Client with the "db" section of application.yaml.
  • Add a counter for metrics.

The DB Client metric counter will be executed only for the select-book statement and it will check how many times it was invoked. At this point, the database is empty, and needs to be initialized. To achieve that, the DB Client can be used to create a table in the database.

Insert a createTable method below the dbClient:
DbClient dbClient = DbClient.builder()
                .config(config.get("db"))
                .addService(DbClientMetrics.counter().statementNames("select-book"))
                .build();

createTable(dbClient);
Copied
Use the DbClient to build a table:
private static void createTable(DbClient dbClient) {
    dbClient.execute(exec -> exec.namedDml("create-table"))     
            .await();
}
Copied
  • Use the "create-table" script to build a table with book name and information.

The createTable is invoked only once and creates an empty table with two columns: name and info. The script is used to boostrap the server, so the await method is called in this particular case because the table must be created before the server starts. A new service can manage requests to interact with this table which represents our library.The services are registered in the createRouting method.

Modify the createRouting method:
import io.helidon.dbclient.health.DbClientHealthCheck;

WebServer server = WebServer.builder(createRouting(config, dbClient))   
                .config(config.get("server"))
                .addMediaSupport(JsonpSupport.create())
                .build();

private static Routing createRouting(Config config, DbClient dbClient) {
    HealthSupport health = HealthSupport.builder()
            .addLiveness(DbClientHealthCheck.create(dbClient))  
            .build();

    return Routing.builder()
            .register(health)                   // Health at "/health"
            .register(MetricsSupport.create())                  // Metrics at "/metrics"
            .register("/greet", new GreetService(config))
            .register("/library", new LibraryService(dbClient))  
            .build();
}
Copied
  • Add dbClient as a parameter of createRouting method.
  • Add Health check to control the application behavior.
  • Register the LibraryService to the Routing.

The library service does not yet exist, but you’ll creat it in the next step of the guide. It has a constructor with the DB Client as a parameter because it will manage the library. The DB Client health check uses the select-book statement from the configuration. As shown above, to create a DB Client health check, call the DbClientHealthCheck.create method and pass the concerned DbClient. Then add it to the health support builder and register it to the routing.

Create the Library service

Create LibraryService class into io.helidon.examples.quickstart.se package.

LibraryService class looks like this:
package io.helidon.examples.quickstart.se;

import io.helidon.common.http.Http;     
import io.helidon.dbclient.DbClient;
import io.helidon.webserver.Routing;
import io.helidon.webserver.ServerRequest;
import io.helidon.webserver.ServerResponse;
import io.helidon.webserver.Service;

public class LibraryService implements Service {

    private final DbClient dbClient;    

    LibraryService(DbClient pDbClient){
        this.dbClient = pDbClient;      
    }
}
Copied
  • Add new import statement
  • Declare the Helidon DB Client
  • A DB Client instance is provided when LibraryService is instantiated.

As the LibraryService implements io.helidon.webserver.Service, the update(Routing) method has to be implemented. It defines application endpoints and Http request which can be reached by clients.

Add update method to LibraryService
@Override
public void update(Routing.Rules rules) {
    rules
        .get("/{name}", this::getBook)      
        .put("/{name}", this::addBook)      
        .delete("/{name}", this::deleteBook)   
        .get("/json/{name}", this::getJsonBook); 
}
Copied
  • Return information about the required book from the database.
  • Add a book to the library.
  • Remove a book from the library.
  • Return the book information in Json format.

To summarize, there is one endpoint that can manipulate books. The number of endpoints and application features can be changed from these rules by creating or modifying methods. {name} is a path parameter for the book name. The architecture of the application is defined, so the next step is to create these features.

Add getBook to the LibraryService:
private void getBook(ServerRequest serverRequest, ServerResponse serverResponse) {
    String bookName = serverRequest.path().param("name");   

    dbClient.execute(exec -> exec.namedGet("select-book", bookName))    
            .thenAccept(row -> {
                if (row.isPresent()) {
                    serverResponse.send(row.get().column("INFO").as(String.class)); 
                } else {
                    serverResponse.status(Http.Status.NOT_FOUND_404)    
                            .send();
                }
            })
            .exceptionally(serverResponse::send);   
}
Copied
  • Get the book name from the path in the URL.
  • Helidon DB Client executes the select-book SQL script from application.yaml.
  • Sends book information to the client.
  • Sends 404 HTTP status if no book was found for the given name.
  • If an exception occurred during the process, it is sent to the client.

The getBook method reach the book from the database and send the information to the client. The name of the book is located into the url path. If the book is not present in the database, a HTTP 404 is sent back. The execute(Function<DbExecute, T> executor) method is called on the dbClient instance to execute one statement. Nevertheless, it is possible to execute a set of tasks into a single execution unit by using inTransaction (Function<DbTransaction, T> executor) method.

DbExecute class provides many builders to create statements such as, DML, insert, update, delete, query and get statements. For each statement there are two builders which can be regrouped in 2 categories. Builders with methods containing Named keyword, they use a statement defined in the configuration file.

And builders without Named keyword, they use a statement passed as an argument. More information on the Helidon DB Client here.

Add getJsonBook to the LibraryService:
private void getJsonBook(ServerRequest serverRequest, ServerResponse serverResponse) {
    String bookName = serverRequest.path().param("name");

    dbClient.execute(exec -> exec.namedGet("select-book", bookName))
            .thenAccept(row -> {
                if (row.isPresent()) {
                    serverResponse.send(row.get().as(JsonObject.class));
                } else {
                    serverResponse.status(Http.Status.NOT_FOUND_404)
                            .send();
                }
            })
            .exceptionally(serverResponse::send);
}
Copied

Instead of sending the INFO content of the targeted book, the getJsonBook method send the whole row of the database as a JsonObject.

Add addBook to the LibraryService:
private void addBook(ServerRequest serverRequest, ServerResponse serverResponse) {
    String bookName = serverRequest.path().param("name");

    serverRequest.content()
            .as(String.class)
            .thenAccept(newValue -> {
                dbClient.execute(exec -> exec.createNamedInsert("insert-book")
                        .addParam("name", bookName)     
                        .addParam("info", newValue)
                        .execute())
                        .thenAccept(count -> serverResponse.status(Http.Status.CREATED_201).send())     
                        .exceptionally(serverResponse::send);
            });
}
Copied
  • The SQL statement requires the book name and its information. They are provided with addParam method.
  • A new book was added to library, so a HTTP 201 code is returned.

When a user adds a new book, it uses HTTP PUT method where the book name is in the URL and the information in the request content. To catch this content, the information is retrieved as a string and then the DB Client execute the insert-book script to add the book to the library. It requires two parameters, the book name and information which are passed to the dbClient thanks to addParam method. A HTTP 201 is sent back as a confirmation if no exception is thrown.

Add deleteBook to LibraryService:
private void deleteBook(ServerRequest serverRequest, ServerResponse serverResponse) {
    String bookName = serverRequest.path().param("name");

    dbClient.execute(exec ->  exec.namedDelete("delete-book", bookName))     
                        .thenAccept(count -> serverResponse.status(Http.Status.NO_CONTENT_204).send())   
                        .exceptionally(serverResponse::send);
}
Copied
  • Execute SQL script from application.yaml to remove a book from the library by its name.
  • The required book was removed, so a HTTP 204 is sent.

To remove a book from the library, use the "delete-book" script in the way than previously. If the book is removed successfully, a HTTP 204 is sent back.

Build and Run the Library Application

The application is ready to be built and run.

Run the following to build the application:
mvn package
Copied

Note that the tests are passing as the GreetService process was not modified. For the purposes of this demonstration, we only added independent new content to the existing application. Make sure H2 is running and start the Helidon quickstart with this command:

Run the application
java -jar target/helidon-quickstart-se.jar
Copied

Once the application starts, check the table LIBRARY is created in the H2 database. To do so, go to the H2 Server console and LIBRARY table should be present in the left column under jdbc:h2:tcp://localhost:9092/~/test. If it is not, try to refresh the page, and it should appear.

Use curl to send request to the application:

Get a book from the library
curl -i http://localhost:8080/library/SomeBook
Copied
HTTP response
HTTP/1.1 404 Not Found
Date: Tue, 12 Jan 2021 14:00:48 +0100
transfer-encoding: chunked
connection: keep-alive
Copied

There is currently no book inside the library, so the application returns a 404. Yet the application created an empty library table. Try to add a new book.

Add a book from the library
curl -i -X PUT -d "Fantasy" http://localhost:8080/library/HarryPotter
Copied
HTTP response
HTTP/1.1 201 Created
Date: Tue, 12 Jan 2021 14:01:08 +0100
transfer-encoding: chunked
connection: keep-alive
Copied

This command creates an HTTP PUT request with the genre Fantasy content at the address http://localhost:8080/library/{book-name}. The 201 code means that Harry Potter book was successfully added to the library. You can now try to get it !

Get Harry Potter from the library
curl -i http://localhost:8080/library/HarryPotter
Copied
HTTP response
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Tue, 12 Jan 2021 14:01:14 +0100
connection: keep-alive
content-length: 6

Fantasy
Copied

The application accepted the request and returned an HTTP 200 OK with the book genre that was added earlier.

Get Harry Potter from the library in Json
curl -i http://localhost:8080/library/json/HarryPotter
Copied
HTTP response
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Tue, 12 Jan 2021 14:01:14 +0100
connection: keep-alive
content-length: 6

{"INFO":"Fantasy"}
Copied

It returns the database row in a Json format for the Harry Potter book. Harry Potter can be removed from the library with the following:

Remove Harry Potter from the library
curl -i -X DELETE http://localhost:8080/library/HarryPotter
Copied
HTTP response
HTTP/1.1 204 No Content
Date: Tue, 12 Jan 2021 14:01:22 +0100
connection: keep-alive
Copied

The book had been removed from the library and confirmed by the 204 HTTP status. To check that the book was correctly deleted, try to get it again.

Get Harry Potter from the library
curl -i http://localhost:8080/library/HarryPotter
Copied
HTTP response
HTTP/1.1 404 Not Found
Date: Tue, 12 Jan 2021 14:00:48 +0100
transfer-encoding: chunked
connection: keep-alive
Copied

The book is not found. We quickly checked, thanks to this suite of command, the application behavior.

Check the health of your application:
curl http://localhost:8080/health
Copied
Response body
{
  "state" : "UP",
  "status" : "UP",
  "name" : "jdbc:h2"
}
Copied

It confirms that the database is UP.

Check the metrics of your application:
curl -H "Accept: application/json" http://localhost:8080/metrics/application
Copied
Response body
{
  "db.counter.select-book" : 4
}
Copied

The select-book statement was invoked four times.

Summary

This guide provided an introduction to the Helidon DB Client’s key features. If you want to learn more, see the Helidon DB Client samples in https://medium.com/helidon/helidon-db-client-e12bbdc85b7.