Amazon QLDB driver for Java – Cookbook reference - Amazon Quantum Ledger Database (Amazon QLDB)

Amazon QLDB driver for Java – Cookbook reference

This section is a reference guide for common use cases of the Amazon QLDB driver for Java. It provides Java code examples that show how to use the driver to run basic create, read, update, and delete (CRUD) operations. It also includes code examples for processing Amazon Ion data. In addition, this guide highlights best practices for making transactions idempotent and implementing uniqueness constraints.

Note

Where applicable, some use cases have different code examples for each supported major version of the QLDB driver for Java.

Importing the driver

The following code example imports the driver, the QLDB session client, Amazon Ion packages, and other related dependencies.

2.x
import com.amazon.ion.IonStruct; import com.amazon.ion.IonSystem; import com.amazon.ion.IonValue; import com.amazon.ion.system.IonSystemBuilder; import software.amazon.awssdk.services.qldbsession.QldbSessionClient; import software.amazon.qldb.QldbDriver; import software.amazon.qldb.Result;
1.x
import com.amazon.ion.IonStruct; import com.amazon.ion.IonSystem; import com.amazon.ion.IonValue; import com.amazon.ion.system.IonSystemBuilder; import com.amazonaws.services.qldbsession.AmazonQLDBSessionClientBuilder; import software.amazon.qldb.PooledQldbDriver; import software.amazon.qldb.Result;

Instantiating the driver

The following code example creates a driver instance that connects to a specified ledger name, and uses specified retry logic with a custom retry limit.

Note

This example also instantiates an Amazon Ion system object (IonSystem). You need this object to process Ion data when running some data operations in this reference. To learn more, see Working with Amazon Ion.

2.x
QldbDriver qldbDriver = QldbDriver.builder() .ledger("vehicle-registration") .transactionRetryPolicy(RetryPolicy .builder() .maxRetries(3) .build()) .sessionClientBuilder(QldbSessionClient.builder()) .build(); IonSystem SYSTEM = IonSystemBuilder.standard().build();
1.x
PooledQldbDriver qldbDriver = PooledQldbDriver.builder() .withLedger("vehicle-registration") .withRetryLimit(3) .withSessionClientBuilder(AmazonQLDBSessionClientBuilder.standard()) .build(); IonSystem SYSTEM = IonSystemBuilder.standard().build();

CRUD operations

QLDB runs create, read, update, and delete (CRUD) operations as part of a transaction.

Warning

As a best practice, make your write transactions strictly idempotent.

Making transactions idempotent

We recommend that you make write transactions idempotent to avoid any unexpected side effects in the case of retries. A transaction is idempotent if it can run multiple times and produce identical results each time.

For example, consider a transaction that inserts a document into a table named Person. The transaction should first check whether or not the document already exists in the table. Without this check, the table might end up with duplicate documents.

Suppose that QLDB successfully commits the transaction on the server side, but the client times out while waiting for a response. If the transaction is not idempotent, the same document could be inserted more than once in the case of a retry.

Using indexes to avoid full table scans

We also recommend that you run statements with a WHERE predicate clause using an equality operator on an indexed field or a document ID; for example, WHERE indexedField = 123 or WHERE indexedField IN (456, 789). Without this indexed lookup, QLDB needs to do a table scan, which can lead to transaction timeouts or optimistic concurrency control (OCC) conflicts.

Note

When you use the IN operator for an indexed field, be sure to enclose the list of values in parentheses. Without the parentheses, the statement invokes a table scan rather than an indexed lookup.

For more information about OCC, see Amazon QLDB concurrency model.

Implicitly created transactions

The QldbDriver.execute method accepts a lambda function that receives an instance of Executor, which you can use to run statements. The Executor instance wraps an implicitly created transaction.

You can run statements within the lambda function by using the Executor.execute method. The driver implicitly commits the transaction when the lambda function returns.

The following sections show how to run basic CRUD operations, specify custom retry logic, and implement uniqueness constraints.

Note

Where applicable, these sections provide code examples of processing Amazon Ion data using both the built-in Ion library and the Jackson Ion mapper library. To learn more, see Working with Amazon Ion.

Creating tables

qldbDriver.execute(txn -> { txn.execute("CREATE TABLE Person"); });

Creating indexes

qldbDriver.execute(txn -> { txn.execute("CREATE INDEX ON Person(GovId)"); });

Reading documents

// Assumes that Person table has documents as follows: // { GovId: "TOYENC486FH", FirstName: "Brent" } qldbDriver.execute(txn -> { Result result = txn.execute("SELECT * FROM Person WHERE GovId = 'TOYENC486FH'"); IonStruct person = (IonStruct) result.iterator().next(); System.out.println(person.get("GovId")); // prints TOYENC486FH System.out.println(person.get("FirstName")); // prints Brent });

Using query parameters

The following code example uses an Ion type query parameter.

qldbDriver.execute(txn -> { Result result = txn.execute("SELECT * FROM Person WHERE GovId = ?", SYSTEM.newString("TOYENC486FH")); IonStruct person = (IonStruct) result.iterator().next(); System.out.println(person.get("GovId")); // prints TOYENC486FH System.out.println(person.get("FirstName")); // prints Brent });

The following code example uses multiple query parameters.

qldbDriver.execute(txn -> { Result result = txn.execute("SELECT * FROM Person WHERE GovId = ? AND FirstName = ?", SYSTEM.newString("TOYENC486FH"), SYSTEM.newString("Brent")); IonStruct person = (IonStruct) result.iterator().next(); System.out.println(person.get("GovId")); // prints TOYENC486FH System.out.println(person.get("FirstName")); // prints Brent });

The following code example uses a list of query parameters.

qldbDriver.execute(txn -> { final List<IonValue> parameters = new ArrayList<>(); parameters.add(SYSTEM.newString("TOYENC486FH")); parameters.add(SYSTEM.newString("ROEE1")); parameters.add(SYSTEM.newString("YH844")); Result result = txn.execute("SELECT * FROM Person WHERE GovId IN (?,?,?)", parameters); IonStruct person = (IonStruct) result.iterator().next(); System.out.println(person.get("GovId")); // prints TOYENC486FH System.out.println(person.get("FirstName")); // prints Brent });
// Assumes that Person table has documents as follows: // {GovId: "TOYENC486FH", FirstName: "Brent" } qldbDriver.execute(txn -> { try { Result result = txn.execute("SELECT * FROM Person WHERE GovId = 'TOYENC486FH'"); Person person = MAPPER.readValue(result.iterator().next(), Person.class); System.out.println(person.getFirstName()); // prints Brent System.out.println(person.getGovId()); // prints TOYENC486FH } catch (IOException e) { e.printStackTrace(); } });

Using query parameters

The following code example uses an Ion type query parameter.

qldbDriver.execute(txn -> { try { Result result = txn.execute("SELECT * FROM Person WHERE GovId = ?", MAPPER.writeValueAsIonValue("TOYENC486FH")); Person person = MAPPER.readValue(result.iterator().next(), Person.class); System.out.println(person.getFirstName()); // prints Brent System.out.println(person.getGovId()); // prints TOYENC486FH } catch (IOException e) { e.printStackTrace(); } });

The following code example uses multiple query parameters.

qldbDriver.execute(txn -> { try { Result result = txn.execute("SELECT * FROM Person WHERE GovId = ? AND FirstName = ?", MAPPER.writeValueAsIonValue("TOYENC486FH"), MAPPER.writeValueAsIonValue("Brent")); Person person = MAPPER.readValue(result.iterator().next(), Person.class); System.out.println(person.getFirstName()); // prints Brent System.out.println(person.getGovId()); // prints TOYENC486FH } catch (IOException e) { e.printStackTrace(); } });

The following code example uses a list of query parameters.

qldbDriver.execute(txn -> { try { final List<IonValue> parameters = new ArrayList<>(); parameters.add(MAPPER.writeValueAsIonValue("TOYENC486FH")); parameters.add(MAPPER.writeValueAsIonValue("ROEE1")); parameters.add(MAPPER.writeValueAsIonValue("YH844")); Result result = txn.execute("SELECT * FROM Person WHERE GovId IN (?,?,?)", parameters); Person person = MAPPER.readValue(result.iterator().next(), Person.class); System.out.println(person.getFirstName()); // prints Brent System.out.println(person.getGovId()); // prints TOYENC486FH } catch (IOException e) { e.printStackTrace(); } });
Note

When you run a query without an indexed lookup, it invokes a full table scan. In this example, we recommend having an index on the GovId field to optimize performance. Without an index on GovId, queries can have more latency and can also lead to OCC conflict exceptions or transaction timeouts.

Inserting documents

The following code example inserts Ion data types.

qldbDriver.execute(txn -> { // Check if a document with GovId:TOYENC486FH exists // This is critical to make this transaction idempotent Result result = txn.execute("SELECT * FROM Person WHERE GovId = ?", SYSTEM.newString("TOYENC486FH")); // Check if there is a result if (!result.iterator().hasNext()) { IonStruct person = SYSTEM.newEmptyStruct(); person.put("GovId").newString("TOYENC486FH"); person.put("FirstName").newString("Brent"); // Insert the document txn.execute("INSERT INTO Person ?", person); } });

The following code example inserts Ion data types.

qldbDriver.execute(txn -> { try { // Check if a document with GovId:TOYENC486FH exists // This is critical to make this transaction idempotent Result result = txn.execute("SELECT * FROM Person WHERE GovId = ?", MAPPER.writeValueAsIonValue("TOYENC486FH")); // Check if there is a result if (!result.iterator().hasNext()) { // Insert the document txn.execute("INSERT INTO Person ?", MAPPER.writeValueAsIonValue(new Person("Brent", "TOYENC486FH"))); } } catch (IOException e) { e.printStackTrace(); } });

This transaction inserts a document into the Person table. Before inserting, it first checks if the document already exists in the table. This check makes the transaction idempotent in nature. Even if you run this transaction multiple times, it will not cause any unintended side effects.

Note

In this example, we recommend having an index on the GovId field to optimize performance. Without an index on GovId, statements can have more latency and can also lead to OCC conflict exceptions or transaction timeouts.

Inserting multiple documents in one statement

To insert multiple documents by using a single INSERT statement, you can pass a parameter of type IonList (explicitly cast as an IonValue) to the statement as follows.

// people is an IonList explicitly cast as an IonValue txn.execute("INSERT INTO People ?", (IonValue) people);

You don't enclose the variable placeholder (?) in double angle brackets ( <<...>> ) when passing an IonList. In manual PartiQL statements, double angle brackets denote an unordered collection known as a bag.

The TransactionExecutor.execute method is overloaded. It accepts a variable number of IonValue arguments (varargs), or a single List<IonValue> argument. In ion-java, IonList is implemented as a List<IonValue>.

Java defaults to the most specific method implementation when you call an overloaded method. In this case, when you pass an IonList parameter, it defaults to the method that takes a List<IonValue>. When invoked, this method implementation passes the IonValue elements of the list as distinct values. So, to invoke the varargs method instead, you must explicitly cast an IonList parameter as an IonValue.

Updating documents

qldbDriver.execute(txn -> { final List<IonValue> parameters = new ArrayList<>(); parameters.add(SYSTEM.newString("John")); parameters.add(SYSTEM.newString("TOYENC486FH")); txn.execute("UPDATE Person SET FirstName = ? WHERE GovId = ?", parameters); });
qldbDriver.execute(txn -> { try { final List<IonValue> parameters = new ArrayList<>(); parameters.add(MAPPER.writeValueAsIonValue("John")); parameters.add(MAPPER.writeValueAsIonValue("TOYENC486FH")); txn.execute("UPDATE Person SET FirstName = ? WHERE GovId = ?", parameters); } catch (IOException e) { e.printStackTrace(); } });
Note

In this example, we recommend having an index on the GovId field to optimize performance. Without an index on GovId, statements can have more latency and can also lead to OCC conflict exceptions or transaction timeouts.

Deleting documents

qldbDriver.execute(txn -> { txn.execute("DELETE FROM Person WHERE GovId = ?", SYSTEM.newString("TOYENC486FH")); });
qldbDriver.execute(txn -> { try { txn.execute("DELETE FROM Person WHERE GovId = ?", MAPPER.writeValueAsIonValue("TOYENC486FH")); } catch (IOException e) { e.printStackTrace(); } });
Note

In this example, we recommend having an index on the GovId field to optimize performance. Without an index on GovId, statements can have more latency and can also lead to OCC conflict exceptions or transaction timeouts.

Running multiple statements in a transaction

// This code snippet is intentionally trivial. In reality you wouldn't do this because you'd // set your UPDATE to filter on vin and insured, and check if you updated something or not. public static boolean InsureCar(QldbDriver qldbDriver, final String vin) { final IonSystem ionSystem = IonSystemBuilder.standard().build(); final IonString ionVin = ionSystem.newString(vin); return qldbDriver.execute(txn -> { Result result = txn.execute( "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", ionVin); if (!result.isEmpty()) { txn.execute("UPDATE Vehicles SET insured = TRUE WHERE vin = ?", ionVin); return true; } return false; }); }

Retry logic

The driver's execute method has a built-in retry mechanism that retries the transaction if a retryable exception occurs (such as timeouts or OCC conflicts).

2.x

The maximum number of retry attempts and the backoff strategy are configurable.

The default retry limit is 4, and the default backoff strategy is DefaultQldbTransactionBackoffStrategy. You can set the retry configuration per driver instance and also per transaction by using an instance of RetryPolicy.

The following code example specifies retry logic with a custom retry limit and a custom backoff strategy for an instance of the driver.

public void retry() { QldbDriver qldbDriver = QldbDriver.builder() .ledger("vehicle-registration") .transactionRetryPolicy(RetryPolicy.builder() .maxRetries(2) .backoffStrategy(new CustomBackOffStrategy()).build()) .sessionClientBuilder(QldbSessionClient.builder()) .build(); } private class CustomBackOffStrategy implements BackoffStrategy { @Override public Duration calculateDelay(RetryPolicyContext retryPolicyContext) { return Duration.ofMillis(1000 * retryPolicyContext.retriesAttempted()); } }

The following code example specifies retry logic with a custom retry limit and a custom backoff strategy for a particular transaction. This configuration for execute overrides the retry logic that is set for the driver instance.

public void retry() { Result result = qldbDriver.execute(txn -> { txn.execute("SELECT * FROM Person WHERE GovId = ?", SYSTEM.newString("TOYENC486FH")); }, RetryPolicy.builder() .maxRetries(2) .backoffStrategy(new CustomBackOffStrategy()) .build()); } private class CustomBackOffStrategy implements BackoffStrategy { // Configuring a custom backoff which increases delay by 1s for each attempt. @Override public Duration calculateDelay(RetryPolicyContext retryPolicyContext) { return Duration.ofMillis(1000 * retryPolicyContext.retriesAttempted()); } }
1.x

The maximum number of retry attempts is configurable. You can configure the retry limit by setting the retryLimit property when initializing PooledQldbDriver.

The default retry limit is 4.

Implementing uniqueness constraints

QLDB does not currently support unique indexes. But it's easy to implement this behavior in your application.

Suppose that you want to implement a uniqueness constraint on the GovId field in the Person table. To do this, you can write a transaction that does the following:

  1. Assert that the table has no existing documents with a specified GovId.

  2. Insert the document if the assertion passes.

If a competing transaction concurrently passes the assertion, only one of the transactions will commit successfully. The other transaction will fail with an OCC conflict exception.

The following code example shows how to implement this uniqueness constraint logic.

qldbDriver.execute(txn -> { Result result = txn.execute("SELECT * FROM Person WHERE GovId = ?", SYSTEM.newString("TOYENC486FH")); // Check if there is a result if (!result.iterator().hasNext()) { IonStruct person = SYSTEM.newEmptyStruct(); person.put("GovId").newString("TOYENC486FH"); person.put("FirstName").newString("Brent"); // Insert the document txn.execute("INSERT INTO Person ?", person); } });
Note

In this example, we recommend having an index on the GovId field to optimize performance. Without an index on GovId, statements can have more latency and can also lead to OCC conflict exceptions or transaction timeouts.

Working with Amazon Ion

There are multiple ways to process Amazon Ion data in QLDB. You can use built-in methods from the Ion library to create and modify documents flexibly as needed. Or, you can use FasterXML's Jackson data format module for Ion to map Ion documents to plain old Java object (POJO) models.

The following sections provide code examples of processing Ion data using both techniques.

Importing the Ion packages

Add the artifact ion-java as a dependency in your Java project.

Gradle
dependencies { compile group: 'com.amazon.ion', name: 'ion-java', version: '1.6.1' }
Maven
<dependencies> <dependency> <groupId>com.amazon.ion</groupId> <artifactId>ion-java</artifactId> <version>1.6.1</version> </dependency> </dependencies>

Import the following Ion packages.

import com.amazon.ion.IonStruct; import com.amazon.ion.IonSystem; import com.amazon.ion.system.IonSystemBuilder;

Add the artifact jackson-dataformat-ion as a dependency in your Java project. QLDB requires version 2.10.0 or later.

Gradle
dependencies { compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-ion', version: '2.10.0' }
Maven
<dependencies> <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-ion</artifactId> <version>2.10.0</version> </dependency> </dependencies>

Import the following Ion packages.

import com.amazon.ion.IonReader; import com.amazon.ion.IonStruct; import com.amazon.ion.system.IonReaderBuilder; import com.amazon.ion.system.IonSystemBuilder; import com.fasterxml.jackson.dataformat.ion.IonObjectMapper; import com.fasterxml.jackson.dataformat.ion.ionvalue.IonValueMapper;

Initializing Ion

IonSystem SYSTEM = IonSystemBuilder.standard().build();
IonObjectMapper MAPPER = new IonValueMapper(IonSystemBuilder.standard().build());

Creating Ion objects

The following code example creates an Ion object by using the IonStruct interface and its built-in methods.

IonStruct ionStruct = SYSTEM.newEmptyStruct(); ionStruct.put("GovId").newString("TOYENC486FH"); ionStruct.put("FirstName").newString("Brent"); System.out.println(ionStruct.toPrettyString()); // prints a nicely formatted copy of ionStruct

Suppose that you have a JSON-mapped model class named Person, as follows.

import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; public static class Person { private final String firstName; private final String govId; @JsonCreator public Person(@JsonProperty("FirstName") final String firstName, @JsonProperty("GovId") final String govId) { this.firstName = firstName; this.govId = govId; } @JsonProperty("FirstName") public String getFirstName() { return firstName; } @JsonProperty("GovId") public String getGovId() { return govId; } }

The following code example creates an IonStruct object from an instance of Person.

IonStruct ionStruct = (IonStruct) MAPPER.writeValueAsIonValue(new Person("Brent", "TOYENC486FH"));

Reading Ion objects

The following code example prints each field of the ionStruct instance.

// ionStruct is an instance of IonStruct System.out.println(ionStruct.get("GovId")); // prints TOYENC486FH System.out.println(ionStruct.get("FirstName")); // prints Brent

The following code example reads an IonStruct object and maps it to an instance of Person.

// ionStruct is an instance of IonStruct IonReader reader = IonReaderBuilder.standard().build(ionStruct); Person person = MAPPER.readValue(reader, Person.class); System.out.println(person.getFirstName()); // prints Brent System.out.println(person.getGovId()); // prints TOYENC486FH

For more information about working with Ion, see the Amazon Ion documentation on GitHub. For more code examples of working with Ion in QLDB, see Working with Amazon Ion data types in Amazon QLDB.