Non-blocking Database Connectivity (NDBC)

Build Status Maven Central Javadoc Join the chat at https://gitter.im/traneio/ndbc

This project's goal is to provide a full asyncronous approach to handle databases.

At the lowest level, the communication with a database is an IO operation. Under the hood, the way to do it would be using a Socket to connect to the database server and exchanging messages, following the proprietary protocol of the database. The nature of this communication is asynchronous, given the principle of sending/receiving messages.

However, that's not how most of IO APIs behave. They return synchronously, blocking the current Thread. What does it mean? Consider the following code:

import java.io.*;
import java.nio.file.*;

File file = File.createTempFile("file", "txt");
List<String> lines = Files.readAllLines(Paths.get(file.toURI()));

For small files, this code wouldn't be a problem. Now let's assume that file.txt has more than 20 gbs. Dealing with such a large file, the code will stop at this line, waiting for all the lines to be read. A better approach to solve this problem is necessary.

Future to the rescue

Future is an abstraction to deal with asynchronicity without blocking threads. The primary usage for Futures on the JVM is to perform IO operations. Bringing this idea to the previous example, the code would be something like:

Future<List<String>> lines = Files.readAllLines(Paths.get(file.toURI()));

In simple words, a Future is the abstraction that allows the code to carry on instead of keep waiting for results, a promise that the expected result will be in place eventually. NDBC is written using Trane.io Future, a High-performance Future implementation for the JVM.

How can be this knowledge about Futures useful when it comes to deal with the database?

An asynchronous alternative to JDBC

As mentioned before, like most of IO APIs, JDBC is synchronous. Working with JDBC, when a query is executed, the return is a ResultSet. Even wrapping this code with Futures is not enough to make it properly asynchronous, JDBC specification is blocking by definition.

NDBC otherwise, was designed to solve this problem.

1 minute example

import io.trane.future.Future;
import io.trane.ndbc.*;
import io.trane.ndbc.postgres.*;
import java.time.Duration;
import java.util.List;

// Create a Config with an Embedded Postgres
Config config = Config.create("io.trane.ndbc.postgres.netty4.DataSourceSupplier", "localhost", 0, "user")
                      .database("test_schema")
                      .password("test")
                      .embedded("io.trane.ndbc.postgres.embedded.EmbeddedSupplier");

// Create a DataSource
DataSource<PreparedStatement, Row> ds = DataSource.fromConfig(config);

// Define a timeout
Duration timeout = Duration.ofSeconds(10);

// Send a query to the db defining a timeout and receiving back a List
List<Row> rows = ds.query("SELECT 1 AS value").get(timeout);

// iterate over awesome strongly typed rows
rows.forEach(row -> System.out.println(row.getLong("value")));

Getting started

The library binaries are distributed through maven central. Click on the maven central badge for information on how to add the library dependency to your project:

Maven Central

Please refer to the Javadoc for detailed information about the library and its features:

Javadoc

Creating a DataSource

From Properties

Properties p = new Properties();
p.setProperty("db.dataSourceSupplierClass", "io.trane.ndbc.postgres.netty4.DataSourceSupplier");
p.setProperty("db.host", "localhost");
p.setProperty("db.port", Integer.toString(5432));
p.setProperty("db.user", "user");
p.setProperty("db.password", "[email protected]");
p.setProperty("db.database", "schema");

DataSource<PreparedStatement, Row> ds = DataSource.fromProperties("db", p);

From Properties file

Using the Properties from previous example:

import java.io.File;
import java.io.FileOutputStream;

File file = File.createTempFile("config", "fromPropertiesFile");
p.store(new FileOutputStream(file), "");

DataSource<PreparedStatement, Row> ds = DataSource.fromPropertiesFile("db", file.getAbsolutePath());

From System Properties

Similar to the first example, but getting the system properties with getProperties:

Properties p = System.getProperties();
p.setProperty("db.dataSourceSupplierClass", "io.trane.ndbc.postgres.netty4.DataSourceSupplier");
p.setProperty("db.host", "localhost");
p.setProperty("db.port", Integer.toString(0));
p.setProperty("db.user", "user");
p.setProperty("db.password", "[email protected]");
p.setProperty("db.database", "schema");

DataSource<PreparedStatement, Row> ds = DataSource.fromSystemProperties("db");

From Jdbc Url

DataSource<PreparedStatement, Row> ds = DataSource.fromJdbcUrl("jdbc:postgresql://user:[email protected]@localhost:5432/schema");

Available configurations

Property Expected value
dataSourceSupplierClass (required) io.trane.ndbc.postgres.netty4.DataSourceSupplier or io.trane.ndbc.mysql.netty4.DataSourceSupplier
host (required) database host
port (required) database port
user (required) user accessing the database
password password of the user accessing the database
database schema name
charset character encoding. If not informed, the default charset of the JVM will be used
poolMaxSize maximum number of connections in the pool
poolMaxWaiters maximum number of waiters for a connection
poolValidationIntervalSeconds frequency to test connections in the pool
connectionTimeoutSeconds maximum time that a connection can remain idle. After that, the pool can close the connection
queryTimeoutSeconds maximum time to execute a query
encodingClasses
nioThreads
embedded
ssl

Querying

Let's use the pre-populated table table_1 as an example:

id description
1 The Amazing Spiderman
2 Batman the Dark Knight
DataSource<PreparedStatement, Row> ds = DataSource.fromConfig(config);

ds.execute("CREATE TABLE table_1 (id integer, description varchar)").get(timeout);
ds.execute("INSERT INTO table_1 VALUES (1, 'The Amazing Spiderman')").get(timeout);
ds.execute("INSERT INTO table_1 VALUES (2, 'Batman the Dark Knight')").get(timeout);

Simple Query

Future<List<Row>> rows = ds.query("SELECT * from table_1");

PreparedStatement

Without parameters

PreparedStatement ps = PreparedStatement.create("SELECT * FROM table_1");

Future<List<Row>> rows = ds.query(ps);

With parameters

PreparedStatement ps = PreparedStatement.create("SELECT * FROM table_1 WHERE id = ?").setInteger(1);

Future<List<Row>> rows = ds.query(ps);

Actions - Insert, Update and Delete

The code to execute actions is similar to the one for queries, the only difference is that instead of using query, we will use execute, which always returns a Future<Long>, with the number of affected rows.

Simple Execute

Future<Long> futureInsertedRows = ds.execute("INSERT INTO table_1 VALUES (10, 'Avengers Assemble!')")

Future<Long> futureUpdatedRows = ds.execute("UPDATE table_1 SET description = 'Go Go Power Rangers'")

Future<Long> futureDeletedRows = ds.execute("DELETE FROM table_1 WHERE id = 10")

PreparedStatement

Without parameters

PreparedStatement ps = PreparedStatement.create("DELETE FROM table_1");

Future<Long> affectedRows = ds.execute(ps);

With parameters

PreparedStatement ps = PreparedStatement.create("DELETE FROM table_1 WHERE id = ?").setInteger(2);

Future<Long> affectedRows = ds.execute(ps);

Transactions

PreparedStatement ps = PreparedStatement.create("DELETE FROM table_1 WHERE id = ?").setInteger(2);

Future<Long> affectedRows = ds.transactional(() -> ds.execute(ps));

Code of Conduct

Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. See CODE_OF_CONDUCT.md for details.

License

See the LICENSE file for details.