Amazon QLDB driver for Go – Quick start tutorial - Amazon Quantum Ledger Database (Amazon QLDB)

Amazon QLDB driver for Go – Quick start tutorial

Important

End of support notice: Existing customers will be able to use Amazon QLDB until end of support on 07/31/2025. For more details, see Migrate an Amazon QLDB Ledger to Amazon Aurora PostgreSQL.

In this tutorial, you learn how to set up a simple application using the latest version of the Amazon QLDB driver for Go. This guide includes steps for installing the driver and short code examples of basic create, read, update, and delete (CRUD) operations.

Prerequisites

Before you get started, make sure that you do the following:

  1. Complete the Prerequisites for the Go driver, if you haven't already done so. This includes signing up for AWS, granting programmatic access for development, and installing Go.

  2. Create a ledger named quick-start.

    To learn how to create a ledger, see Basic operations for Amazon QLDB ledgers or Step 1: Create a new ledger in Getting started with the console.

Step 1: Install the driver

Ensure that your project is using Go modules to install project dependencies.

In your project directory, enter the following go get command.

$ go get -u github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver

Installing the driver also installs its dependencies, including the AWS SDK for Go v2 and Amazon Ion packages.

Step 2: Import the packages

Import the following AWS packages.

import ( "context" "fmt" "github.com/amzn/ion-go/ion" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/qldbSession" "github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver" )

Step 3: Initialize the driver

Initialize an instance of the driver that connects to the ledger named quick-start.

cfg, err := config.LoadDefaultConfig(context.TODO()) if err != nil { panic(err) } qldbSession := qldbsession.NewFromConfig(cfg, func(options *qldbsession.Options) { options.Region = "us-east-1" }) driver, err := qldbdriver.New( "quick-start", qldbSession, func(options *qldbdriver.DriverOptions) { options.LoggerVerbosity = qldbdriver.LogInfo }) if err != nil { panic(err) } defer driver.Shutdown(context.Background())
Note

In this code example, replace us-east-1 with the AWS Region where you created your ledger.

Step 4: Create a table and index

The following code example shows how to run CREATE TABLE and CREATE INDEX statements.

_, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) { _, err := txn.Execute("CREATE TABLE People") if err != nil { return nil, err } // When working with QLDB, it's recommended to create an index on fields we're filtering on. // This reduces the chance of OCC conflict exceptions with large datasets. _, err = txn.Execute("CREATE INDEX ON People (firstName)") if err != nil { return nil, err } _, err = txn.Execute("CREATE INDEX ON People (age)") if err != nil { return nil, err } return nil, nil }) if err != nil { panic(err) }

This code creates a table named People, and indexes for the firstName and age fields on that table. Indexes are required to optimize query performance and help to limit optimistic concurrency control (OCC) conflict exceptions.

Step 5: Insert a document

The following code examples show how to run an INSERT statement. QLDB supports the PartiQL query language (SQL compatible) and the Amazon Ion data format (superset of JSON).

Using literal PartiQL

The following code inserts a document into the People table using a string literal PartiQL statement.

_, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) { return txn.Execute("INSERT INTO People {'firstName': 'Jane', 'lastName': 'Doe', 'age': 77}") }) if err != nil { panic(err) }

Using Ion data types

Similar to Go's built-in JSON package, you can marshal and unmarshal Go data types to and from Ion.

  1. Suppose that you have the following Go structure named Person.

    type Person struct { FirstName string `ion:"firstName"` LastName string `ion:"lastName"` Age int `ion:"age"` }
  2. Create an instance of Person.

    person := Person{"John", "Doe", 54}

    The driver marshals an Ion-encoded text representation of person for you.

    Important

    For marshal and unmarshal to work properly, the field names of the Go data structure must be exported (first letter capitalized).

  3. Pass the person instance to the transaction's Execute method.

    _, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) { return txn.Execute("INSERT INTO People ?", person) }) if err != nil { panic(err) }

    This example uses a question mark (?) as a variable placeholder to pass the document information to the statement. When you use placeholders, you must pass an Ion-encoded text value.

    Tip

    To insert multiple documents by using a single INSERT statement, you can pass a parameter of type list to the statement as follows.

    // people is a list txn.Execute("INSERT INTO People ?", people)

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

Step 6: Query the document

The following code example shows how to run a SELECT statement.

p, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) { result, err := txn.Execute("SELECT firstName, lastName, age FROM People WHERE age = 54") if err != nil { return nil, err } // Assume the result is not empty hasNext := result.Next(txn) if !hasNext && result.Err() != nil { return nil, result.Err() } ionBinary := result.GetCurrentData() temp := new(Person) err = ion.Unmarshal(ionBinary, temp) if err != nil { return nil, err } return *temp, nil }) if err != nil { panic(err) } var returnedPerson Person returnedPerson = p.(Person) if returnedPerson != person { fmt.Print("Queried result does not match inserted struct") }

This example queries your document from the People table, assumes that the result set isn't empty, and returns your document from the result.

Step 7: Update the document

The following code example shows how to run an UPDATE statement.

person.Age += 10 _, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) { return txn.Execute("UPDATE People SET age = ? WHERE firstName = ?", person.Age, person.FirstName) }) if err != nil { panic(err) }

Step 8: Query the updated document

The following code example queries the People table by firstName and returns all of the documents in the result set.

p, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) { result, err := txn.Execute("SELECT firstName, lastName, age FROM People WHERE firstName = ?", person.FirstName) if err != nil { return nil, err } var people []Person for result.Next(txn) { ionBinary := result.GetCurrentData() temp := new(Person) err = ion.Unmarshal(ionBinary, temp) if err != nil { return nil, err } people = append(people, *temp) } if result.Err() != nil { return nil, result.Err() } return people, nil }) if err != nil { panic(err) } var people []Person people = p.([]Person) updatedPerson := Person{"John", "Doe", 64} if people[0] != updatedPerson { fmt.Print("Queried result does not match updated struct") }

Step 9: Drop the table

The following code example shows how to run a DROP TABLE statement.

_, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) { return txn.Execute("DROP TABLE People") }) if err != nil { panic(err) }

Running the complete application

The following code example is the complete version of the application. Instead of doing the previous steps individually, you can also copy and run this code example from start to end. This application demonstrates some basic CRUD operations on the ledger named quick-start.

Note

Before you run this code, make sure that you don't already have an active table named People in the quick-start ledger.

package main import ( "context" "fmt" "github.com/amzn/ion-go/ion" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/qldbsession" "github.com/awslabs/amazon-qldb-driver-go/v2/qldbdriver" ) func main() { awsSession := session.Must(session.NewSession(aws.NewConfig().WithRegion("us-east-1"))) qldbSession := qldbsession.New(awsSession) driver, err := qldbdriver.New( "quick-start", qldbSession, func(options *qldbdriver.DriverOptions) { options.LoggerVerbosity = qldbdriver.LogInfo }) if err != nil { panic(err) } defer driver.Shutdown(context.Background()) _, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) { _, err := txn.Execute("CREATE TABLE People") if err != nil { return nil, err } // When working with QLDB, it's recommended to create an index on fields we're filtering on. // This reduces the chance of OCC conflict exceptions with large datasets. _, err = txn.Execute("CREATE INDEX ON People (firstName)") if err != nil { return nil, err } _, err = txn.Execute("CREATE INDEX ON People (age)") if err != nil { return nil, err } return nil, nil }) if err != nil { panic(err) } _, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) { return txn.Execute("INSERT INTO People {'firstName': 'Jane', 'lastName': 'Doe', 'age': 77}") }) if err != nil { panic(err) } type Person struct { FirstName string `ion:"firstName"` LastName string `ion:"lastName"` Age int `ion:"age"` } person := Person{"John", "Doe", 54} _, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) { return txn.Execute("INSERT INTO People ?", person) }) if err != nil { panic(err) } p, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) { result, err := txn.Execute("SELECT firstName, lastName, age FROM People WHERE age = 54") if err != nil { return nil, err } // Assume the result is not empty hasNext := result.Next(txn) if !hasNext && result.Err() != nil { return nil, result.Err() } ionBinary := result.GetCurrentData() temp := new(Person) err = ion.Unmarshal(ionBinary, temp) if err != nil { return nil, err } return *temp, nil }) if err != nil { panic(err) } var returnedPerson Person returnedPerson = p.(Person) if returnedPerson != person { fmt.Print("Queried result does not match inserted struct") } person.Age += 10 _, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) { return txn.Execute("UPDATE People SET age = ? WHERE firstName = ?", person.Age, person.FirstName) }) if err != nil { panic(err) } p, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) { result, err := txn.Execute("SELECT firstName, lastName, age FROM People WHERE firstName = ?", person.FirstName) if err != nil { return nil, err } var people []Person for result.Next(txn) { ionBinary := result.GetCurrentData() temp := new(Person) err = ion.Unmarshal(ionBinary, temp) if err != nil { return nil, err } people = append(people, *temp) } if result.Err() != nil { return nil, result.Err() } return people, nil }) if err != nil { panic(err) } var people []Person people = p.([]Person) updatedPerson := Person{"John", "Doe", 64} if people[0] != updatedPerson { fmt.Print("Queried result does not match updated struct") } _, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) { return txn.Execute("DROP TABLE People") }) if err != nil { panic(err) } }