

本文属于机器翻译版本。若本译文内容与英语原文存在差异，则一律以英文原文为准。

# Amazon QLDB 驱动程序入门
<a name="getting-started-driver"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

本节包含帮助您了解如何使用 Amazon QLDB 驱动程序开发 Amazon QLDB 的实践教程。该驱动程序建立在 AWS SDK 之上，该软件开发工具包支持与 [QLDB](api-reference.md) API 的交互。

**QLDB 会话抽象**

该驱动程序在事务数据 API 中提供了高级抽象层（*QLDB Session*）。[它通过[SendCommand](https://docs.aws.amazon.com/qldb/latest/developerguide/API_QLDB-Session_SendCommand.html)管理 API 调用，简化了在账本数据上运行 PartiQL 语句的过程。](ql-reference.md)这些 API 调用需要驱动程序为您处理的多项参数，包括会话管理、事务管理以及出现错误时的重试策略。该驱动程序还进行了性能优化，并采用了与 QLDB 交互的最佳实践。

**注意**  
要与 [Amazon QLDB API 参考中列出的资源管理 API](https://docs.aws.amazon.com/qldb/latest/developerguide/API_Operations_Amazon_QLDB.html) 操作进行交互，您可以直接使用 AWS 软件开发工具包而不是驱动程序。管理 API 仅用于管理分类账资源和非事务性数据操作，例如导出、流式传输和数据验证。

**Amazon Ion 支持**

此外，该驱动程序使用 [Amazon Ion](ion.md) 库来支持在运行事务时处理 Ion 数据。这些库还负责计算 Ion 值的哈希值。QLDB 需要这些 Ion 哈希来检查数据事务请求的完整性。

**驱动程序术语**

该工具之所以被称为*驱动程序*，是因为它与其他提供开发者友好接口的数据库驱动程序相当。这些驱动程序同样封装了将一组标准命令和函数转换为服务的低级 API 所需的特定调用的逻辑。

该驱动程序是开源的 GitHub ，可用于以下编程语言：
+ [Java 驱动程序](getting-started.java.md)
+ [.NET 驱动程序](getting-started.dotnet.md)
+ [Go 驱动程序](getting-started.golang.md)
+ [Node.js 驱动程序](getting-started.nodejs.md)
+ [Python 驱动程序](getting-started.python.md)

有关所有支持的编程语言的一般驱动程序信息以及其他教程，请参阅以下主题：
+ [驱动程序会话管理](driver-session-management.md)
+ [驱动程序推荐](driver.best-practices.md)
+ [驱动程序重试策略](driver-retry-policy.md)
+ [常见错误](driver-errors.md)
+ [应用程序教程示例](getting-started-sample-app.md)
+ [使用 Amazon Ion](driver-working-with-ion.md)
+ [获取 PartiQL 语句统计信息](working.statement-stats.md)

# 适用于 Java 的 Amazon QLDB 驱动程序
<a name="getting-started.java"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

要处理账本中的数据，您可以使用提供的驱动程序从 Java 应用程序连接到 Amazon QLDB。 AWS 以下主题介绍了如何开始使用 Java 上 QLDB 驱动程序。

**Topics**
+ [

## 驱动程序资源
](#getting-started.java.resources)
+ [

## 先决条件
](#getting-started.java.prereqs)
+ [

## 设置您的默认 AWS 凭证和区域
](#getting-started.java.credentials)
+ [

## 安装
](#getting-started.java.install)
+ [快速入门教程](driver-quickstart-java.md)
+ [说明书参考](driver-cookbook-java.md)

## 驱动程序资源
<a name="getting-started.java.resources"></a>

有关 Java 驱动程序支持功能的更多信息，请参阅以下资源：
+ API 参考：[2.x](https://javadoc.io/doc/software.amazon.qldb/amazon-qldb-driver-java/latest/), [1.x](https://javadoc.io/doc/software.amazon.qldb/amazon-qldb-driver-java/1.1.0/index.html)
+ [驱动程序源代码 (GitHub)](https://github.com/awslabs/amazon-qldb-driver-java)
+ [示例应用程序源代码 (GitHub)](https://github.com/aws-samples/amazon-qldb-dmv-sample-java)
+ [账本加载器框架 (GitHub)](https://github.com/awslabs/amazon-qldb-ledger-load-java)
+ [Amazon Ion 代码示例](ion.code-examples.md)

## 先决条件
<a name="getting-started.java.prereqs"></a>

开始使用适用于 Java 的 QLDB 驱动程序之前，您必须执行以下操作:

1. 按照中的 AWS 设置说明进行操作[访问 Amazon QLDB](accessing.md)。这包括以下这些：

   1. 报名参加 AWS.

   1. 创建具有适当 QLDB 权限的用户。

   1. 授权以编程方式访问开发。

1. 通过下载并安装以下内容来设置一个 Java 开发环境：

   1. Java SE Development Kit 8，例如 [Amazon Corretto](https://docs.aws.amazon.com/corretto/latest/corretto-8-ug/downloads-list.html) 8。

   1. （可选）您选择的 Java 集成式开发环境（IDE），例如 [Eclipse](http://www.eclipse.org) 或 [IntelliJ](https://www.jetbrains.com/idea/)。

1. 为 b 适用于 Java 的 AWS SDK y 配置您的开发环境[设置您的默认 AWS 凭证和区域](#getting-started.java.credentials)。

接下来，您可下载完整的教程示例应用程序，也可以只在 Java 项目中安装驱动程序并运行短代码示例。
+ 要在现有项目中安装 QLDB 驱动程序和 适用于 Java 的 AWS SDK ，请继续。[安装](#getting-started.java.install)
+ 要设置项目并运行演示分类账上基本数据事务的简短代码示例，请参阅 [快速入门教程](driver-quickstart-java.md)。
+ 要在完整的教程示例应用程序中运行更深入的数据和管理 API 操作示例，请参阅 [Java 教程](getting-started.java.tutorial.md)。

## 设置您的默认 AWS 凭证和区域
<a name="getting-started.java.credentials"></a>

QLDB 驱动程序和[适用于 Java 的 AWS SDK](https://aws.amazon.com/sdk-for-java)底层要求您在运行时 AWS 向应用程序提供凭证。本指南中的代码示例假设您使用 AWS 凭证文件，如 *AWS SDK for Java 2.x 《开发者指南》* 中的 [设置默认凭证和区域](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/setup.html#setup-credentials) 所述。

作为这些步骤的一部分，您还应设置默认值 AWS 区域 以确定默认 QLDB 终端节点。在默认的 AWS 区域中，代码示例连接到 QLDB。有关 QLDB 区域的完整列表，请参阅AWS 一般参考**中的 [Amazon QLDB 端点和限额](https://docs.aws.amazon.com/general/latest/gr/qldb.html)。

以下是名为的 AWS 凭据文件的示例`~/.aws/credentials`，其中波浪号字符 (`~`) 表示您的主目录。

```
[default] 
aws_access_key_id = your_access_key_id
aws_secret_access_key = your_secret_access_key
```

用您自己的 AWS 凭证值代替值*your\$1access\$1key\$1id*和*your\$1secret\$1access\$1key*。

## 安装
<a name="getting-started.java.install"></a>

QLDB 支持以下 Java 驱动程序版本及 AWS 其 SDK 依赖关系。


****  

| 驱动程序版本 | AWS SDK | 状态 | 最新发布日期 | 
| --- | --- | --- | --- | 
| [1.x](https://search.maven.org/artifact/software.amazon.qldb/amazon-qldb-driver-java/1.1.0/jar) | 适用于 Java 的 AWS SDK 1.x | 量产版 | 2020 年 3 月 20 日 | 
| [2.x](https://search.maven.org/artifact/software.amazon.qldb/amazon-qldb-driver-java/2.3.1/jar) | AWS SDK for Java 2.x | 量产版 | 2021 年 6 月 4 日 | 

要安装 QLDB 驱动程序，我们建议使用依赖项管理系统，比如 Gradle 或 Maven。例如，将以下构件作为依赖项添加到您的 Java 项目中。

------
#### [ 2.x ]

**Gradle**

在您的 `build.gradle` 配置文件中添加此依赖项。

```
dependencies {
    compile group: 'software.amazon.qldb', name: 'amazon-qldb-driver-java', version: '2.3.1'
}
```

**Maven**

在您的 `pom.xml` 配置文件中添加此依赖项。

```
<dependencies>
  <dependency>
    <groupId>software.amazon.qldb</groupId>
    <artifactId>amazon-qldb-driver-java</artifactId>
    <version>2.3.1</version>
  </dependency>
</dependencies>
```

此工件自动包含 AWS SDK for Java 2.x 核心模块、[Amazon Ion](ion.md) 库和其他必需的依赖项。

------
#### [ 1.x ]

**Gradle**

在您的 `build.gradle` 配置文件中添加此依赖项。

```
dependencies {
    compile group: 'software.amazon.qldb', name: 'amazon-qldb-driver-java', version: '1.1.0'
}
```

**Maven**

在您的 `pom.xml` 配置文件中添加此依赖项。

```
<dependencies>
  <dependency>
    <groupId>software.amazon.qldb</groupId>
    <artifactId>amazon-qldb-driver-java</artifactId>
    <version>1.1.0</version>
  </dependency>
</dependencies>
```

此工件自动包含 适用于 Java 的 AWS SDK 核心模块、[Amazon Ion](ion.md) 库和其他必需的依赖项。

**重要**  
**Amazon Ion 命名空间** - 在应用程序中导入 Amazon Ion 类时，必须使用命名空间 `com.amazon.ion` 下的软件包。 适用于 Java 的 AWS SDK 依赖于`software.amazon.ion`命名空间下的另一个 Ion 包，但这是一个与 QLDB 驱动程序不兼容的旧包。

------

有关如何在分类账上运行基本数据事务的简短代码示例，请参阅 [说明书参考](driver-cookbook-java.md)。

### 其他可选库
<a name="getting-started.java.install-optional"></a>

另外，您还可以在项目中添加以下有用的库。这些构件是 [Java 教程](getting-started.java.tutorial.md) 示例应用程序中必需的依赖项。

1. [aws-java-sdk-qldb](https://search.maven.org/artifact/com.amazonaws/aws-java-sdk-qldb/1.11.785/jar)— 的 QLDB 模块。 适用于 Java 的 AWS SDK QLDB 支持的最低版本为 `1.11.785`。

   在您的应用程序中使用此模块可以直接与 [Amazon QLDB API 参考](api-reference.md) 中列出的管理 API 操作进行交互。

1. [jackson-dataformat-ion](https://search.maven.org/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-ion/2.10.0/bundle)— FasterXML 的 Jackson 数据格式模块，适用于 Ion。示例应用程序需要版本 `2.10.0` 或更高版本。

------
#### [ Gradle ]

将这些依赖项添加到您的 `build.gradle` 配置文件中。

```
dependencies {
    compile group: 'com.amazonaws', name: 'aws-java-sdk-qldb', version: '1.11.785'
    compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-ion', version: '2.10.0'
}
```

------
#### [ Maven ]

将这些依赖项添加到您的 `pom.xml` 配置文件中。

```
<dependencies>
  <dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-java-sdk-qldb</artifactId>
    <version>1.11.785</version>
  </dependency>
  <dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-ion</artifactId>
    <version>2.10.0</version>
  </dependency>
</dependencies>
```

------

# 适用于 Java 的 Amazon QLDB 驱动程序 — 快速入门教程
<a name="driver-quickstart-java"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在本教程中，您将学习如何使用适用于 Java 的最新版 Amazon QLDB 驱动程序来设置简单应用程序。本指南包括安装驱动程序的步骤以及*创建、读取、更新和删除*（CRUD）的基本操作的简短代码示例。有关在完整示例应用程序中演示这些操作的更深入的示例，请参阅 [Java 教程](getting-started.java.tutorial.md)。

**Topics**
+ [

## 先决条件
](#driver-quickstart-java.prereqs)
+ [

## 步骤 1：设置您的项目
](#driver-quickstart-java.step-1)
+ [

## 第 2 步：初始化驱动程序
](#driver-quickstart-java.step-2)
+ [

## 第 3 步：创建表和索引
](#driver-quickstart-java.step-3)
+ [

## 第 4 步：插入文档
](#driver-quickstart-java.step-4)
+ [

## 第 5 步：查询文档
](#driver-quickstart-java.step-5)
+ [

## 第 6 步：更新文档
](#driver-quickstart-java.step-6)
+ [

## 运行完整的应用程序
](#driver-quickstart-java.complete)

## 先决条件
<a name="driver-quickstart-java.prereqs"></a>

在开始之前，请务必执行以下操作：

1. 请为 Java 驱动程序完成（[先决条件](getting-started.java.md#getting-started.java.prereqs)如果尚未执行此操作）。这包括注册 AWS、授予开发所需的编程访问权限以及安装 Java 集成开发环境 (IDE)。

1. 创建一个名为 `quick-start` 分类账。

   要了解如何创建分类账，请参阅*控制台入门*中的 [Amazon QLDB 分类账的基本操作](ledger-management.basics.md) 或 [第 1 步：创建新分类账](getting-started-step-1.md)。

## 步骤 1：设置您的项目
<a name="driver-quickstart-java.step-1"></a>

首先，您需要设置您的 Java 项目。我们建议在本教程中使用 [Maven](https://maven.apache.org/index.html) 依赖项管理系统。

**注意**  
如果您使用的 IDE 具有自动执行这些设置步骤的功能，则可以直接跳到 [第 2 步：初始化驱动程序](#driver-quickstart-java.step-2)。

1. 为应用程序创建一个文件夹。

   ```
   $ mkdir myproject
   $ cd myproject
   ```

1. 输入以下命令从 Maven 模板初始化项目。根据*project-package*需要*project-name**maven-template*用您自己的值替换、和。

   ```
   $ mvn archetype:generate
     -DgroupId=project-package \
     -DartifactId=project-name \
     -DarchetypeArtifactId=maven-template \
     -DinteractiveMode=false
   ```

   对于*maven-template*，你可以使用基本的 Maven 模板：`maven-archetype-quickstart`

1. 要将[ Java 的 QLDB 驱动程序](https://search.maven.org/artifact/software.amazon.qldb/amazon-qldb-driver-java/2.3.1/jar)添加为项目依赖项，请导航到新创建的 `pom.xml` 文件并添加以下构件。

   ```
   <dependency>
       <groupId>software.amazon.qldb</groupId>
       <artifactId>amazon-qldb-driver-java</artifactId>
       <version>2.3.1</version>
   </dependency>
   ```

   此构件自动包含 [AWS SDK for Java 2.x](https://aws.amazon.com/sdk-for-java) 核心模块、[Amazon Ion](ion.md) 库和其他必需的依赖项。您的 `pom.xml` 文件应类似于如下所示：

   ```
   <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
     <modelVersion>4.0.0</modelVersion>
     <groupId>software.amazon.qldb</groupId>
     <artifactId>qldb-quickstart</artifactId>
     <packaging>jar</packaging>
     <version>1.0-SNAPSHOT</version>
     <name>qldb-quickstart</name>
     <url>http://maven.apache.org</url>
     <dependencies>
       <dependency>
         <groupId>junit</groupId>
         <artifactId>junit</artifactId>
         <version>3.8.1</version>
         <scope>test</scope>
       </dependency>
       <dependency>
           <groupId>software.amazon.qldb</groupId>
           <artifactId>amazon-qldb-driver-java</artifactId>
           <version>2.3.1</version>
       </dependency>
     </dependencies>
   </project>
   ```

1. 打开 `App.java`文件。

   然后，按以下步骤中逐步添加代码示例，尝试一些基本的 CRUD 操作。或者，您可以跳过本 step-by-step教程，改为运行[完整的应用程序](#driver-quickstart-java.complete)。

## 第 2 步：初始化驱动程序
<a name="driver-quickstart-java.step-2"></a>

初始化连接到名为 `quick-start` 的分类账的驱动程序实例。将以下代码添加到您的 `App.java`文件。

```
import java.util.*;
import com.amazon.ion.*;
import com.amazon.ion.system.*;
import software.amazon.awssdk.services.qldbsession.QldbSessionClient;
import software.amazon.qldb.*;

public final class App {
    public static IonSystem ionSys = IonSystemBuilder.standard().build();
    public static QldbDriver qldbDriver;

    public static void main(final String... args) {
        System.out.println("Initializing the driver");
        qldbDriver = QldbDriver.builder()
            .ledger("quick-start")
            .transactionRetryPolicy(RetryPolicy
                .builder()
                .maxRetries(3)
                .build())
            .sessionClientBuilder(QldbSessionClient.builder())
            .build();
    }
}
```

## 第 3 步：创建表和索引
<a name="driver-quickstart-java.step-3"></a>

以下代码示例显示如何运行 `CREATE TABLE` 和 `CREATE INDEX`。

在 `main` 方法中，添加以下代码，创建名为 `People` 的表以及该表上的 `lastName` 字段的索引。[索引](ql-reference.create-index.md)是优化查询性能和帮助限制[乐观并发控制（OCC）冲突异常](concurrency.md)所必需的。

```
// Create a table and an index in the same transaction
qldbDriver.execute(txn -> {
    System.out.println("Creating a table and an index");
    txn.execute("CREATE TABLE People");
    txn.execute("CREATE INDEX ON People(lastName)");
});
```

## 第 4 步：插入文档
<a name="driver-quickstart-java.step-4"></a>

以下代码示例显示如何运行 `INSERT` 语句。QLDB 支持 [PartiQL](ql-reference.md) 查询语言（兼容 SQL）和 [Amazon Ion](ion.md) 数据格式（JSON 的超集）。

添加以下代码，在 `People` 表格中插入文档。

```
// Insert a document
qldbDriver.execute(txn -> {
    System.out.println("Inserting a document");
    IonStruct person = ionSys.newEmptyStruct();
    person.put("firstName").newString("John");
    person.put("lastName").newString("Doe");
    person.put("age").newInt(32);
    txn.execute("INSERT INTO People ?", person);
});
```

此示例使用问号（`?`）作为变量占位符，将文档信息传递给语句。使用占位符时，必须传递一个 `IonValue` 类型的值。

**提示**  
要使用单个[INSERT](ql-reference.insert.md)语句插入多个文档，可以向该语句传递一个类型的参数 [IonList](driver-working-with-ion.md#driver-ion-list)（显式转换为`IonValue`），如下所示。  

```
// people is an IonList explicitly cast as an IonValue
txn.execute("INSERT INTO People ?", (IonValue) people);
```
传递 `IonList` 列表时，不要将变量占位符（`?`）包括在双尖括号（ `<<...>>` ）内。在手动 PartiQL 语句中，双尖括号表示名为*bag*的无序集合。

## 第 5 步：查询文档
<a name="driver-quickstart-java.step-5"></a>

以下代码示例显示如何运行 `SELECT` 语句。

添加以下代码，用于从 `People` 表格中查询文档。

```
// Query the document
qldbDriver.execute(txn -> {
    System.out.println("Querying the table");
    Result result = txn.execute("SELECT * FROM People WHERE lastName = ?",
            ionSys.newString("Doe"));
    IonStruct person = (IonStruct) result.iterator().next();
    System.out.println(person.get("firstName")); // prints John
    System.out.println(person.get("lastName")); // prints Doe
    System.out.println(person.get("age")); // prints 32
});
```

## 第 6 步：更新文档
<a name="driver-quickstart-java.step-6"></a>

以下代码示例显示如何运行 `UPDATE` 语句。

1. 添加以下代码，通过更新 `age` 到 `42` 来更新 `People` 表中的文档。

   ```
   // Update the document
   qldbDriver.execute(txn -> {
       System.out.println("Updating the document");
       final List<IonValue> parameters = new ArrayList<>();
       parameters.add(ionSys.newInt(42));
       parameters.add(ionSys.newString("Doe"));
       txn.execute("UPDATE People SET age = ? WHERE lastName = ?", parameters);
   });
   ```

1. 再次查询文档以查看更新的值。

   ```
   // Query the updated document
   qldbDriver.execute(txn -> {
       System.out.println("Querying the table for the updated document");
       Result result = txn.execute("SELECT * FROM People WHERE lastName = ?",
               ionSys.newString("Doe"));
       IonStruct person = (IonStruct) result.iterator().next();
       System.out.println(person.get("firstName")); // prints John
       System.out.println(person.get("lastName")); // prints Doe
       System.out.println(person.get("age")); // prints 32
   });
   ```

1. 使用 Maven 或你的 IDE 来编译和运行该 `App.java` 文件。

## 运行完整的应用程序
<a name="driver-quickstart-java.complete"></a>

以下代码示例是 `App.java` 应用程序的完整版本。您还可以从头到尾复制并运行此代码示例，而不必单独执行前面的步骤。此应用程序演示了对名为 `quick-start` 的分类账的一些基本的 CRUD 操作。

**注意**  
在运行此代码之前，请确保 `quick-start` 分类账中还没有名为 `People` 的活动表。  
在第一行，替换为中*project-package*用于 Maven 命令的`groupId`[步骤 1：设置您的项目](#driver-quickstart-java.step-1)值。

```
package project-package;

import java.util.*;
import com.amazon.ion.*;
import com.amazon.ion.system.*;
import software.amazon.awssdk.services.qldbsession.QldbSessionClient;
import software.amazon.qldb.*;

public class App {
    public static IonSystem ionSys = IonSystemBuilder.standard().build();
    public static QldbDriver qldbDriver;

    public static void main(final String... args) {
        System.out.println("Initializing the driver");
        qldbDriver = QldbDriver.builder()
            .ledger("quick-start")
            .transactionRetryPolicy(RetryPolicy
                .builder()
                .maxRetries(3)
                .build())
            .sessionClientBuilder(QldbSessionClient.builder())
            .build();

        // Create a table and an index in the same transaction
        qldbDriver.execute(txn -> {
            System.out.println("Creating a table and an index");
            txn.execute("CREATE TABLE People");
            txn.execute("CREATE INDEX ON People(lastName)");
        });
        
        // Insert a document
        qldbDriver.execute(txn -> {
            System.out.println("Inserting a document");
            IonStruct person = ionSys.newEmptyStruct();
            person.put("firstName").newString("John");
            person.put("lastName").newString("Doe");
            person.put("age").newInt(32);
            txn.execute("INSERT INTO People ?", person);
        });
        
        // Query the document
        qldbDriver.execute(txn -> {
            System.out.println("Querying the table");
            Result result = txn.execute("SELECT * FROM People WHERE lastName = ?",
                    ionSys.newString("Doe"));
            IonStruct person = (IonStruct) result.iterator().next();
            System.out.println(person.get("firstName")); // prints John
            System.out.println(person.get("lastName")); // prints Doe
            System.out.println(person.get("age")); // prints 32
        });
        
        // Update the document
        qldbDriver.execute(txn -> {
            System.out.println("Updating the document");
            final List<IonValue> parameters = new ArrayList<>();
            parameters.add(ionSys.newInt(42));
            parameters.add(ionSys.newString("Doe"));
            txn.execute("UPDATE People SET age = ? WHERE lastName = ?", parameters);
        });
        
        // Query the updated document
        qldbDriver.execute(txn -> {
            System.out.println("Querying the table for the updated document");
            Result result = txn.execute("SELECT * FROM People WHERE lastName = ?",
                    ionSys.newString("Doe"));
            IonStruct person = (IonStruct) result.iterator().next();
            System.out.println(person.get("firstName")); // prints John
            System.out.println(person.get("lastName")); // prints Doe
            System.out.println(person.get("age")); // prints 42
        });
    }
}
```

使用 Maven 或你的 IDE 来编译和运行该 `App.java` 文件。

# Java 的 Amazon QLDB 驱动程序 — 说明书参考
<a name="driver-cookbook-java"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

本参考指南展示了 Java 的 Amazon QLDB 驱动程序的常见用例。它提供了 Java 代码示例，演示了如何使用该驱动程序运行基本的*创建、读取、更新和删除*（CRUD）操作。它还包括用于处理 Amazon Ion 数据的代码示例。此外，本指南还重点介绍了使事务具有幂等性和实现唯一性约束的最佳实践。

**注意**  
在适用的情况下，某些用例对于 Java 的 QLDB 驱动程序的每个支持主要版本都配备不同代码示例。

**Contents**
+ [

## 导入驱动程序
](#cookbook-java.importing)
+ [

## 实例化驱动程序
](#cookbook-java.instantiating)
+ [

## CRUD 操作
](#cookbook-java.crud)
  + [

### 创建表
](#cookbook-java.crud.creating-tables)
  + [

### 创建索引
](#cookbook-java.crud.creating-indexes)
  + [

### 阅读文档
](#cookbook-java.crud.reading)
  + [

### 插入文档
](#cookbook-java.crud.inserting)
    + [

#### 在一条语句内插入多个文档
](#cookbook-java.crud.inserting.multiple)
  + [

### 更新文档
](#cookbook-java.crud.updating)
  + [

### 删除文档
](#cookbook-java.crud.deleting)
  + [

### 在一个事务中运行多条语句
](#cookbook-java.crud.multi-statement)
  + [

### 重试逻辑
](#cookbook-java.crud.retry-logic)
  + [

### 实现唯一限制
](#cookbook-java.crud.uniqueness-constraints)
+ [

## 使用 Amazon Ion
](#cookbook-java.ion)
  + [

### 导入 Ion 软件包
](#cookbook-java.ion.importing)
  + [

### Ion 初始化
](#cookbook-java.ion.initializing)
  + [

### 创建 Ion 对象
](#cookbook-java.ion.creating)
  + [

### 读取 Ion 对象
](#cookbook-java.ion.reading)

## 导入驱动程序
<a name="cookbook-java.importing"></a>

以下代码示例导入驱动程序、QLDB 会话客户端、Amazon Ion 软件包以及其他相关依赖项。

------
#### [ 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;
```

------

## 实例化驱动程序
<a name="cookbook-java.instantiating"></a>

以下代码示例创建了连接到指定分类账名称的驱动程序实例，并使用具有自定义重试限制的指定[重试逻辑](#cookbook-java.crud.retry-logic)。

**注意**  
此示例还实例化了 Amazon Ion 系统对象（`IonSystem`）。在本参考中运行某些数据操作时，您需要此对象来处理 Ion 数据。要了解更多信息，请参阅 [使用 Amazon Ion](#cookbook-java.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 操作
<a name="cookbook-java.crud"></a>

QLDB 作为事务的一部分运行*创建、读取、更新和删除*（CRUD）操作。

**警告**  
作为最佳实践，使写事务严格地幂等。

**使事务幂等**

我们建议将写事务设置为幂等，以避免重试时出现任何意想不到的副作用。如果事务可以运行多次并每次都产生相同的结果，则事务是*幂等的*。

例如，假设有一个事务，要将文档插入名为 `Person` 的表中。事务应该首先检查文档是否已经存在于表中。如果没有这种检查，表最终可能会有重复的文档。

假设 QLDB 在服务器端成功提交了事务，但客户端在等待响应时超时。如果事务不是幂等的，则在重试的情况下可以多次插入相同的文档。

**使用索引避免全表扫描**

我们还建议您在索引字段或文档 ID 上使用*相等*运算符来运行带有`WHERE`谓词子句的语句；例如，`WHERE indexedField = 123`或 `WHERE indexedField IN (456, 789)`。如果没有这种索引查找，QLDB 需要进行表扫描，这可能会导致事务超时或*乐观并发控制*（OCC）冲突。

有关 OCC 的更多信息，请参阅[Amazon QLDB 并发模型](concurrency.md)。

**隐式创建的事务**

[QldbDriver.execut](https://javadoc.io/doc/software.amazon.qldb/amazon-qldb-driver-java/latest/software/amazon/qldb/QldbDriver.html) e 方法接受一个 lambda 函数，该函数接收一个 E [xecutor](https://javadoc.io/doc/software.amazon.qldb/amazon-qldb-driver-java/latest/software/amazon/qldb/Executor.html) 的实例，你可以用它来运行语句。`Executor`实例封装了隐式创建的事务。

您可以使用`Executor.execute`方法在 lambda 函数中运行语句。当 lambda 函数返回时，驱动程序会隐式提交事务。

以下各节介绍如何运行基本的 CRUD 操作、指定自定义重试逻辑以及如何实现唯一性约束。

**注意**  
在适用的情况下，这些部分提供了使用内置 Ion 库和 Jackson Ion 映射器库处理 Amazon Ion 数据代码示例。要了解更多信息，请参阅 [使用 Amazon Ion](#cookbook-java.ion)。

**Contents**
+ [

### 创建表
](#cookbook-java.crud.creating-tables)
+ [

### 创建索引
](#cookbook-java.crud.creating-indexes)
+ [

### 阅读文档
](#cookbook-java.crud.reading)
+ [

### 插入文档
](#cookbook-java.crud.inserting)
  + [

#### 在一条语句内插入多个文档
](#cookbook-java.crud.inserting.multiple)
+ [

### 更新文档
](#cookbook-java.crud.updating)
+ [

### 删除文档
](#cookbook-java.crud.deleting)
+ [

### 在一个事务中运行多条语句
](#cookbook-java.crud.multi-statement)
+ [

### 重试逻辑
](#cookbook-java.crud.retry-logic)
+ [

### 实现唯一限制
](#cookbook-java.crud.uniqueness-constraints)

### 创建表
<a name="cookbook-java.crud.creating-tables"></a>

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

### 创建索引
<a name="cookbook-java.crud.creating-indexes"></a>

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

### 阅读文档
<a name="cookbook-java.crud.reading"></a>

```
// 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
});
```

**使用查询参数**

以下代码示例使用 Ion 类型查询参数。

```
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
});
```

以下代码示例使用了多个查询参数。

```
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
});
```

以下代码示例使用查询参数列表。

```
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
});
```

#### 使用 Jackson 映射程序
<a name="cookbook-java.crud.reading.jackson"></a>

```
// 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();
    }
});
```

**使用查询参数**

以下代码示例使用 Ion 类型查询参数。

```
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();
    }
});
```

以下代码示例使用了多个查询参数。

```
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();
    }
});
```

以下代码示例使用查询参数列表。

```
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();
    }
});
```

**注意**  
当您在没有索引查找的情况下运行查询时，它会调用全表扫描。在此示例中，我们建议在`GovId`字段上设置 [索引](ql-reference.create-index.md)以优化性能。如果不开启`GovId`索引，查询可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

### 插入文档
<a name="cookbook-java.crud.inserting"></a>

以下代码示例插入 Ion 数据类型。

```
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);
    }
});
```

#### 使用 Jackson 映射程序
<a name="cookbook-java.crud.inserting.jackson"></a>

以下代码示例插入 Ion 数据类型。

```
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();
    }
});
```

此事务将文档插入 `Person` 表中。在插入之前，它首先检查文档是否已存在于表格内。**此检查使事务本质上是幂等。**即使您多次运行此事务，也不会造成任何异常副作用。

**注意**  
在此示例中，我们建议在 `GovId` 字段上设置索引以优化性能。如果不开启`GovId`索引，语句可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

#### 在一条语句内插入多个文档
<a name="cookbook-java.crud.inserting.multiple"></a>

要使用单个[INSERT](ql-reference.insert.md)语句插入多个文档，可以向该语句传递一个类型的参数 [IonList](driver-working-with-ion.md#driver-ion-list)（显式转换为`IonValue`），如下所示。

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

传递 `IonList` 列表时，不要将变量占位符（`?`）包括在双尖括号（ `<<...>>` ）内。在手动 PartiQL 语句中，双尖括号表示名为*bag*的无序集合。

##### 为什么需要显式转换？
<a name="cookbook-java.crud.inserting.multiple.cast"></a>

[TransactionExecutor.execute](https://javadoc.io/doc/software.amazon.qldb/amazon-qldb-driver-java/latest/software/amazon/qldb/TransactionExecutor.html) 方法已过载。它接受可变数量的 `IonValue`参数（*varargs*）或单个`List<IonValue>`参数。在 [ion-java](https://www.javadoc.io/doc/com.amazon.ion/ion-java/latest/index.html) 中，`IonList` 被实现为`List<IonValue>`。

当您调用重载方法，Java 默认其为最具体的实施方法。在这种情况下，当您传递 `IonList` 参数时，它默认为采用`List<IonValue>`。调用时，此方法实现会将列表中的`IonValue`元素作为不同的值传递。因此，要改为调用 varargs 方法，必须将`IonList`参数显式转换为`IonValue`。

### 更新文档
<a name="cookbook-java.crud.updating"></a>

```
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);
});
```

#### 使用 Jackson 映射程序
<a name="cookbook-java.crud.updating.jackson"></a>

```
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();
    }
});
```

**注意**  
在此示例中，我们建议在 `GovId` 字段上设置索引以优化性能。如果不开启`GovId`索引，语句可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

### 删除文档
<a name="cookbook-java.crud.deleting"></a>

```
qldbDriver.execute(txn -> {
    txn.execute("DELETE FROM Person WHERE GovId = ?",
            SYSTEM.newString("TOYENC486FH"));
});
```

#### 使用 Jackson 映射程序
<a name="cookbook-java.crud.deleting.jackson"></a>

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

**注意**  
在此示例中，我们建议在 `GovId` 字段上设置索引以优化性能。如果不开启`GovId`索引，语句可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

### 在一个事务中运行多条语句
<a name="cookbook-java.crud.multi-statement"></a>

```
// 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;
    });
}
```

### 重试逻辑
<a name="cookbook-java.crud.retry-logic"></a>

驱动程序 `execute` 方法具有内置的重试机制，如果发生可重试的异常（例如超时或 OCC 冲突），该机制可以重试事务。

------
#### [ 2.x ]

最大重试次数和退避策略为可配置。

默认重试限制为`4`，默认退避策略为。[DefaultQldbTransactionBackoffStrategy](https://javadoc.io/doc/software.amazon.qldb/amazon-qldb-driver-java/latest/software/amazon/qldb/DefaultQldbTransactionBackoffStrategy.html)您可以使用的实例为每个驱动程序实例以及每个事务设置重试配置。[RetryPolicy](https://javadoc.io/doc/software.amazon.qldb/amazon-qldb-driver-java/latest/software/amazon/qldb/RetryPolicy.html)

以下代码示例使用自定义重试限制和驱动程序实例的自定义退避策略指定重试逻辑。

```
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());
    }
}
```

以下代码示例使用自定义重试限制和自定义回退策略指定重试逻辑。此`execute`配置将覆盖为驱动程序实例设置的重试逻辑。

```
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 ]

最大重试次数。您可以通过在初始化`PooledQldbDriver` 时设置 `retryLimit` 属性来配置重试限制。

默认重试限制为 `4`。

------

### 实现唯一限制
<a name="cookbook-java.crud.uniqueness-constraints"></a>

QLDB 不支持唯一索引，但您可在应用程序中实现此行为。

假设您要对 `Person` 表中的 `GovId` 字段实现唯一性约束。据此，可以编写执行以下操作的事务：

1. 断言该表中没有指定 `GovId` 的现有文档。

1. 如果断言通过，请插入文档。

如果一个竞争事务同时通过断言，则只有一个事务将成功提交。另一笔事务将失败，并显示 OCC 冲突异常。

以下代码示例显示了如何实现此唯一约束。

```
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);
    }
});
```

**注意**  
在此示例中，我们建议在 `GovId` 字段上设置索引以优化性能。如果不开启`GovId`索引，语句可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

## 使用 Amazon Ion
<a name="cookbook-java.ion"></a>

可通过多种方法在 QLDB 中处理 Amazon Ion 数据。您可根据需要使用 [Ion 库](https://github.com/amzn/ion-java) 中的内置方法，灵活创建和修改文档。或者，您可以使用 FasterXML 的 [Jackson Ion 数据格式模块](https://github.com/FasterXML/jackson-dataformats-binary/tree/master/ion)，以将 Ion 文档映射至*普通旧 Java 对象*（POJO）模型。

以下各节提供了使用这两种技术处理 Ion 数据的代码示例。

**Contents**
+ [

### 导入 Ion 软件包
](#cookbook-java.ion.importing)
+ [

### Ion 初始化
](#cookbook-java.ion.initializing)
+ [

### 创建 Ion 对象
](#cookbook-java.ion.creating)
+ [

### 读取 Ion 对象
](#cookbook-java.ion.reading)

### 导入 Ion 软件包
<a name="cookbook-java.ion.importing"></a>

将工件 [ion-java](https://search.maven.org/artifact/com.amazon.ion/ion-java/1.6.1/bundle) 作为依赖项添加至您的 Java 项目。

------
#### [ 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>
```

------

导入以下 Ion 软件包。

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

#### 使用 Jackson 映射程序
<a name="cookbook-java.ion.importing.jackson"></a>

将构件[jackson-dataformat-ion](https://search.maven.org/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-ion/2.10.0/bundle)作为依赖项添加到 Java 项目中。QLDB 需要 `2.10.0` 版本或以上版本。

------
#### [ 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>
```

------

导入以下 Ion 软件包。

```
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;
```

### Ion 初始化
<a name="cookbook-java.ion.initializing"></a>

```
IonSystem SYSTEM = IonSystemBuilder.standard().build();
```

#### 使用 Jackson 映射程序
<a name="cookbook-java.ion.initializing.jackson"></a>

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

### 创建 Ion 对象
<a name="cookbook-java.ion.creating"></a>

以下代码示例使用 `IonStruct` 接口及其内置方法创建 Ion 对象。

```
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
```

#### 使用 Jackson 映射程序
<a name="cookbook-java.ion.creating.jackson"></a>

假设您有名为 JSON 映射的模型类`Person`，如下所示。

```
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;
    }
}
```

以下代码示例根据`IonStruct`实例创建对象`Person`。

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

### 读取 Ion 对象
<a name="cookbook-java.ion.reading"></a>

以下代码示例打印 `ionStruct` 实例的每个字段。

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

#### 使用 Jackson 映射程序
<a name="cookbook-java.ion.reading.jackson"></a>

以下代码示例读取一个 `IonStruct` 对象并将其映射到的实例`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
```

有关使用 Ion 的更多信息，请参阅上的 [Amazon Ion 文档](http://amzn.github.io/ion-docs/) GitHub。有关在 QLDB 中使用 Ion 的更多代码示例，请参阅[使用 Amazon QLDB 中的 Amazon Ion 数据类型](driver-working-with-ion.md)。

# 适用于 .NET 的 Amazon QLDB 驱动程序
<a name="getting-started.dotnet"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

要处理账本中的数据，您可以使用提供的驱动程序从 Microsoft .NET 应用程序连接到 Amazon QLDB。 AWS 分类账定位于 **.NET 标准 2.0**。更具体地说，它支持 **.NET Core（LTS）2.1\$1 **和 **.NE T Framework 4.5.2**\$1。有关兼容性的信息，请参阅 *Microsoft 文档*网站上的 [.NET 标准](https://docs.microsoft.com/en-us/dotnet/standard/net-standard)。

我们强烈建议使用 *Ion 对象映射器*来完全无需在 Amazon Ion 类型和原生 C\$1 类型之间进行手动转换。

以下主题介绍了如何开始使用适用于 .NET 的 QLDB 驱动程序。

**Topics**
+ [

## 驱动程序资源
](#getting-started.dotnet.resources)
+ [

## 先决条件
](#getting-started.dotnet.prereqs)
+ [

## 安装
](#getting-started.dotnet.install)
+ [快速入门教程](driver-quickstart-dotnet.md)
+ [说明书参考](driver-cookbook-dotnet.md)

## 驱动程序资源
<a name="getting-started.dotnet.resources"></a>

有关 .NET 驱动程序支持功能的更多信息，请参阅以下资源：
+ [API 参考](https://amazon-qldb-docs.s3.amazonaws.com/drivers/dotnet/1.4.1/api/Amazon.QLDB.Driver.html)
+ [驱动程序源代码 (GitHub)](https://github.com/awslabs/amazon-qldb-driver-dotnet)
+ [示例应用程序源代码 (GitHub)](https://github.com/aws-samples/amazon-qldb-dmv-sample-dotnet)
+ [Amazon Ion 说明书](http://amzn.github.io/ion-docs/guides/cookbook.html)
+ [离子物体映射器 () GitHub](https://github.com/amzn/ion-object-mapper-dotnet)

## 先决条件
<a name="getting-started.dotnet.prereqs"></a>

开始使用适用于 .NET 的 QLDB 驱动程序之前，您必须执行以下操作:

1. 按照中的 AWS 设置说明进行操作[访问 Amazon QLDB](accessing.md)。这包括以下这些：

   1. 报名参加 AWS.

   1. 创建具有适当 QLDB 权限的用户。

   1. 授权以编程方式访问开发。

1. 从[微软 .NET 下载](https://dotnet.microsoft.com/download)网站下载并安装 .NET Core SDK 2.1 或更高版本。

1. （可选）安装您选择的集成式开发环境（IDE），例如 Visual Studio、Mac 版 Visual Studio 或 Visual Studio Code。你可以从[微软 Visual Studio](https://visualstudio.microsoft.com/) 网站下载这些文件。

1. 配置您的开发环境用于 [适用于 .NET 的 AWS SDK](https://aws.amazon.com/sdk-for-net)：

   1. 设置您的 AWS 凭证。我们建议创建共享的凭证文件。

      有关说明，请参阅*适用于 .NET 的 AWS SDK 开发者指南*中的[使用凭证文件配置 AWS 证书](https://docs.aws.amazon.com/sdk-for-net/latest/developer-guide/net-dg-config-creds.html#creds-file)。

   1. 设置您的默认 AWS 区域。要了解如何操作，请参阅[AWS 区域 选择](https://docs.aws.amazon.com/sdk-for-net/latest/developer-guide/net-dg-region-selection.html)。

      有关可用区域的完整列表，请参阅 *AWS 一般参考* 中的 [Amazon QLDB 端点和限额](https://docs.aws.amazon.com/general/latest/gr/qldb.html)。

接下来，您可以设置基本的示例应用程序并运行简短的代码示例，也可以将驱动程序安装到现有的 .NET 项目中。
+ 要在现有项目中安装 QLDB 驱动程序和 适用于 .NET 的 AWS SDK ，请继续。[安装](#getting-started.dotnet.install)
+ 要设置项目并运行演示分类账上基本数据事务的简短代码示例，请参阅 [快速入门教程](driver-quickstart-dotnet.md)。

## 安装
<a name="getting-started.dotnet.install"></a>

使用 NuGet 包管理器安装适用于.NET 的 QLDB 驱动程序。我们建议使用 Visual Studio 或您选择的 IDE 向项目添加依赖关系。驱动程序包名称为 [Amazon.QLDB.Driver](https://www.nuget.org/packages/amazon.qldb.driver)。

例如，在 Visual Studio 中，在 “**工具**” 菜单上打开 Pack **NuGet age Manager 控制台**。然后在 `PM>` 提示符处，输入以下命令。

```
PM> Install-Package Amazon.QLDB.Driver
```

安装驱动程序还会安装其依赖项，包括 适用于 .NET 的 AWS SDK 和 [Amazon Ion](ion.md) 软件包。

### 安装 Ion 对象映射器
<a name="getting-started.dotnet.install.mapper"></a>

适用于 .NET 的 QLDB 驱动程序 1.3.0 版引入了无需使用 Amazon Ion 即可接受和返回原生 C\$1 数据类型的支持。要使用此功能，请将以下软件包添加到您的项目中。
+ [Amazon.QLDB.Driver.Serialization](https://www.nuget.org/packages/Amazon.QLDB.Driver.Serialization/) - 一个可以将 Ion 值映射到 C\$1 *普通旧 CLR 对象*（POCO）的库，反之亦然。此 Ion 对象映射器可让您的应用程序直接与原生 C\$1 数据类型进行交互，而无需使用 Ion。有关如何使用此库的简短指南，请参阅存储库中的 s [erialization.md](https://github.com/awslabs/amazon-qldb-driver-dotnet/blob/master/SERIALIZATION.md) 文件。 GitHub `awslabs/amazon-qldb-driver-dotnet`

要安装此程序包，请输入以下命令：

```
PM> Install-Package Amazon.QLDB.Driver.Serialization
```

有关如何在分类账上运行基本数据事务的简短代码示例，请参阅 [说明书参考](driver-cookbook-dotnet.md)。

# 适用于.NET 的 Amazon QLDB 驱动程序 — 快速入门教程
<a name="driver-quickstart-dotnet"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在本教程中，您将学习如何使用适用于.NET 的 Amazon QLDB 驱动程序来设置简单应用程序。本指南包括安装驱动程序的步骤以及*创建、读取、更新和删除*（CRUD）的基本操作的简短代码示例。

**Topics**
+ [

## 先决条件
](#driver-quickstart-dotnet.prereqs)
+ [

## 步骤 1：设置您的项目
](#driver-quickstart-dotnet.step-1)
+ [

## 第 2 步：初始化驱动程序
](#driver-quickstart-dotnet.step-2)
+ [

## 第 3 步：创建表和索引
](#driver-quickstart-dotnet.step-3)
+ [

## 第 4 步：插入文档
](#driver-quickstart-dotnet.step-4)
+ [

## 第 5 步：查询文档
](#driver-quickstart-dotnet.step-5)
+ [

## 第 6 步：更新文档
](#driver-quickstart-dotnet.step-6)
+ [

## 运行完整的应用程序
](#driver-quickstart-dotnet.complete)

## 先决条件
<a name="driver-quickstart-dotnet.prereqs"></a>

在开始之前，请务必执行以下操作：

1. 请为 .NET 驱动程序完成（[先决条件](getting-started.dotnet.md#getting-started.dotnet.prereqs)如果尚未执行此操作）。这包括注册 AWS、授予开发所需的编程访问权限以及安装.NET Core SDK。

1. 创建一个名为 `quick-start` 分类账。

   要了解如何创建分类账，请参阅*控制台入门*中的 [Amazon QLDB 分类账的基本操作](ledger-management.basics.md) 或 [第 1 步：创建新分类账](getting-started-step-1.md)。

## 步骤 1：设置您的项目
<a name="driver-quickstart-dotnet.step-1"></a>

首先，您需要设置您的 .NET 项目。

1. 要创建和运行模板应用程序，请在终端上输入以下`dotnet`命令，例如 *bash *PowerShell**、或*命令提示符*。

   ```
   $ dotnet new console --output Amazon.QLDB.QuickStartGuide
   $ dotnet run --project Amazon.QLDB.QuickStartGuide
   ```

   这个模版将创建一个名为`Amazon.QLDB.QuickStartGuide`的文件夹。在该文件夹中，它会创建一个同名项目和一个名为 `Program.cs` 的文件。该程序包含显示输出的代码 `Hello World!`。

1. 使用 NuGet 包管理器安装适用于.NET 的 QLDB 驱动程序。我们建议使用 Visual Studio 或您选择的 IDE 向项目添加依赖关系。驱动程序包名称为 [Amazon.QLDB.Driver](https://www.nuget.org/packages/amazon.qldb.driver)。
   + 例如，在 Visual Studio 中，在 “**工具**” 菜单上打开 Pack **NuGet age Manager 控制台**。然后在 `PM>` 提示符处，输入以下命令。

     ```
     PM> Install-Package Amazon.QLDB.Driver
     ```
   + 或者，您可以在终端上输入以下命令。

     ```
     $ cd Amazon.QLDB.QuickStartGuide
     $ dotnet add package Amazon.QLDB.Driver
     ```

   安装驱动程序还会安装其依赖项，包括[适用于 .NET 的 AWS SDK](https://aws.amazon.com/sdk-for-net)和 [Amazon Ion](ion.md) 库。

1. 安装驱动程序的序列化库。

   ```
   PM> Install-Package Amazon.QLDB.Driver.Serialization
   ```

1. 打开 `Program.cs`文件。

   然后，按以下步骤中逐步添加代码示例，尝试一些基本的 CRUD 操作。或者，您可以跳过本 step-by-step教程，改为运行[完整的应用程序](#driver-quickstart-dotnet.complete)。

**注意**  
**在同步和异步之间进行选择 APIs** — 驱动程序提供同步和异步 APIs。对于能够在不阻塞的情况下处理多个请求的高要求应用程序，我们建议使用异步 APIs 以提高性能。该驱动程序提供了同步 APIs 功能，为同步编写的现有代码库提供了额外的便利。  
本教程包括同步和异步代码示例。有关更多信息 APIs，请参阅 API 文档中的[IQldb驱动程序](https://amazon-qldb-docs.s3.amazonaws.com/drivers/dotnet/1.4.1/api/Amazon.QLDB.Driver.IQldbDriver.html)和[IAsyncQldbDriver](https://amazon-qldb-docs.s3.amazonaws.com/drivers/dotnet/1.4.1/api/Amazon.QLDB.Driver.IAsyncQldbDriver.html)接口。
**处理 Amazon Ion 数据** – 本教程提供了默认使用 [Ion 对象映射器](https://github.com/amzn/ion-object-mapper-dotnet) 处理 Amazon Ion 数据的代码示例。QLDB 在.NET 驱动程序的 1.3.0 版本中引入 Ion 对象映射器。在适用的情况下，本教程还提供使用标准 [Ion 库](https://github.com/amzn/ion-dotnet) 作为替代方案的代码示例。要了解更多信息，请参阅 [使用 Amazon Ion](driver-cookbook-dotnet.md#cookbook-dotnet.ion)。

## 第 2 步：初始化驱动程序
<a name="driver-quickstart-dotnet.step-2"></a>

初始化连接到名为 `quick-start` 的分类账的驱动程序实例。将以下代码添加到您的 `Program.cs`文件。

------
#### [ Async ]

```
using Amazon.QLDB.Driver;
using Amazon.QLDB.Driver.Generic;
using Amazon.QLDB.Driver.Serialization;

namespace Amazon.QLDB.QuickStartGuide
{
    class Program
    {
        public class Person
        {
            public string FirstName { get; set; }

            public string LastName { get; set; }

            public int Age { get; set; }

            public override string ToString()
            {
                return FirstName + ", " + LastName + ", " + Age.ToString();
            }
        }

        static async Task Main(string[] args)
        {
            Console.WriteLine("Create the async QLDB driver");
            IAsyncQldbDriver driver = AsyncQldbDriver.Builder()
                .WithLedger("quick-start")
                .WithSerializer(new ObjectSerializer())
                .Build();
        }
    }
}
```

------
#### [ Sync ]

```
using Amazon.QLDB.Driver;
using Amazon.QLDB.Driver.Generic;
using Amazon.QLDB.Driver.Serialization;

namespace Amazon.QLDB.QuickStartGuide
{
    class Program
    {
        public class Person
        {
            public string FirstName { get; set; }

            public string LastName { get; set; }

            public int Age { get; set; }

            public override string ToString()
            {
                return FirstName + ", " + LastName + ", " + Age.ToString();
            }
        }

        static void Main(string[] args)
        {
            Console.WriteLine("Create the sync QLDB driver");
            IQldbDriver driver = QldbDriver.Builder()
                .WithLedger("quick-start")
                .WithSerializer(new ObjectSerializer())
                .Build();
        }
    }
}
```

------

### 使用 Ion 库
<a name="driver-quickstart-dotnet.step-2.ion-library"></a>

------
#### [ Async ]

```
using System;
using System.Threading.Tasks;
using Amazon.IonDotnet.Tree;
using Amazon.IonDotnet.Tree.Impl;
using Amazon.QLDB.Driver;
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;

namespace Amazon.QLDB.QuickStartGuide
{
    class Program
    {
        static IValueFactory valueFactory = new ValueFactory();

        static async Task Main(string[] args)
        {
            Console.WriteLine("Create the async QLDB driver");
            IAsyncQldbDriver driver = AsyncQldbDriver.Builder()
                .WithLedger("quick-start")
                .Build();
        }
    }
}
```

------
#### [ Sync ]

```
using System;
using Amazon.IonDotnet.Tree;
using Amazon.IonDotnet.Tree.Impl;
using Amazon.QLDB.Driver;

namespace Amazon.QLDB.QuickStartGuide
{
    class Program
    {
        static IValueFactory valueFactory = new ValueFactory();

        static void Main(string[] args)
        {
            Console.WriteLine("Create the sync QLDB Driver");
            IQldbDriver driver = QldbDriver.Builder()
                .WithLedger("quick-start")
                .Build();
        }
    }
}
```

------

## 第 3 步：创建表和索引
<a name="driver-quickstart-dotnet.step-3"></a>

在本教程至*步骤 6* 的其余部分中，您需要将以下代码示例附加到前面的代码示例中。

在此步骤中，以下代码显示了如何运行 `CREATE TABLE` 和 `CREATE INDEX` 语句。它会创建一个名为 `Person` 的表，并为该表上的 `firstName` 字段创建索引。[索引](ql-reference.create-index.md)是优化查询性能和帮助限制[乐观并发控制（OCC）冲突异常](concurrency.md)所必需的。

------
#### [ Async ]

```
Console.WriteLine("Creating the table and index");

// Creates the table and the index in the same transaction.
// Note: Any code within the lambda can potentially execute multiple times due to retries.
// For more information, see: https://docs.aws.amazon.com/qldb/latest/developerguide/driver-retry-policy
await driver.Execute(async txn =>
{
    await txn.Execute("CREATE TABLE Person");
    await txn.Execute("CREATE INDEX ON Person(firstName)");
});
```

------
#### [ Sync ]

```
Console.WriteLine("Creating the tables and index");

// Creates the table and the index in the same transaction.
// Note: Any code within the lambda can potentially execute multiple times due to retries.
// For more information, see: https://docs.aws.amazon.com/qldb/latest/developerguide/driver-retry-policy
driver.Execute(txn =>
{
    txn.Execute("CREATE TABLE Person");
    txn.Execute("CREATE INDEX ON Person(firstName)");
});
```

------

## 第 4 步：插入文档
<a name="driver-quickstart-dotnet.step-4"></a>

以下代码示例显示如何运行 `INSERT` 语句。QLDB 支持 [PartiQL](ql-reference.md) 查询语言（兼容 SQL）和 [Amazon Ion](ion.md) 数据格式（JSON 的超集）。

添加以下代码，在 `Person` 表格中插入文档。

------
#### [ Async ]

```
Console.WriteLine("Inserting a document");

Person myPerson = new Person {
    FirstName = "John",
    LastName = "Doe",
    Age = 32
};

await driver.Execute(async txn =>
{
    IQuery<Person> myQuery = txn.Query<Person>("INSERT INTO Person ?", myPerson);
    await txn.Execute(myQuery);
});
```

------
#### [ Sync ]

```
Console.WriteLine("Inserting a document");

Person myPerson = new Person {
    FirstName = "John",
    LastName = "Doe",
    Age = 32
};

driver.Execute(txn =>
{
    IQuery<Person> myQuery = txn.Query<Person>("INSERT INTO Person ?", myPerson);
    txn.Execute(myQuery);
});
```

------

### 使用 Ion 库
<a name="driver-quickstart-dotnet.step-4.ion-library"></a>

------
#### [ Async ]

```
Console.WriteLine("Inserting a document");

// This is one way of creating Ion values. We can also use an IonLoader.
// For more details, see: https://docs.aws.amazon.com/qldb/latest/developerguide/driver-cookbook-dotnet.html#cookbook-dotnet.ion
IIonValue ionPerson = valueFactory.NewEmptyStruct();
ionPerson.SetField("firstName", valueFactory.NewString("John"));
ionPerson.SetField("lastName", valueFactory.NewString("Doe"));
ionPerson.SetField("age", valueFactory.NewInt(32));

await driver.Execute(async txn =>
{
    await txn.Execute("INSERT INTO Person ?", ionPerson);
});
```

------
#### [ Sync ]

```
Console.WriteLine("Inserting a document");

// This is one way of creating Ion values, we can also use an IonLoader.
// For more details, see: https://docs.aws.amazon.com/qldb/latest/developerguide/driver-cookbook-dotnet.html#cookbook-dotnet.ion
IIonValue ionPerson = valueFactory.NewEmptyStruct();
ionPerson.SetField("firstName", valueFactory.NewString("John"));
ionPerson.SetField("lastName", valueFactory.NewString("Doe"));
ionPerson.SetField("age", valueFactory.NewInt(32));

driver.Execute(txn =>
{
    txn.Execute("INSERT INTO Person ?", ionPerson);
});
```

------

**提示**  
要使用单个 [INSERT](ql-reference.insert.md) 语句插入多个文档，可以向该语句传递一个[Ion 列表](driver-working-with-ion.md#driver-ion-list)类型的参数，如下所示。  

```
// people is an Ion list
txn.Execute("INSERT INTO Person ?", people);
```
传递 Ion 列表时，不要将变量占位符（`?`）括在双尖括号（`<<...>>`）内。在手动 PartiQL 语句中，双尖括号表示名为*bag*的无序集合。

## 第 5 步：查询文档
<a name="driver-quickstart-dotnet.step-5"></a>

以下代码示例显示如何运行 `SELECT` 语句。

添加以下代码，用于从 `Person` 表格中查询文档。

------
#### [ Async ]

```
Console.WriteLine("Querying the table");

// The result from driver.Execute() is buffered into memory because once the
// transaction is committed, streaming the result is no longer possible.
IAsyncResult<Person> selectResult = await driver.Execute(async txn =>
{
    IQuery<Person> myQuery = txn.Query<Person>("SELECT * FROM Person WHERE FirstName = ?", "John");
    return await txn.Execute(myQuery);
});

await foreach (Person person in selectResult)
{
    Console.WriteLine(person);
    // John, Doe, 32
}
```

------
#### [ Sync ]

```
Console.WriteLine("Querying the table");

// The result from driver.Execute() is buffered into memory because once the
// transaction is committed, streaming the result is no longer possible.
IResult<Person> selectResult = driver.Execute(txn =>
{
    IQuery<Person> myQuery = txn.Query<Person>("SELECT * FROM Person WHERE FirstName = ?", "John");
    return txn.Execute(myQuery);
});

foreach (Person person in selectResult)
{
    Console.WriteLine(person);
    // John, Doe, 32
}
```

------

### 使用 Ion 库
<a name="driver-quickstart-dotnet.step-5.ion-library"></a>

------
#### [ Async ]

```
Console.WriteLine("Querying the table");

IIonValue ionFirstName = valueFactory.NewString("John");

// The result from driver.Execute() is buffered into memory because once the
// transaction is committed, streaming the result is no longer possible.
IAsyncResult selectResult = await driver.Execute(async txn =>
{
    return await txn.Execute("SELECT * FROM Person WHERE firstName = ?", ionFirstName);
});

await foreach (IIonValue row in selectResult)
{
    Console.WriteLine(row.GetField("firstName").StringValue);
    Console.WriteLine(row.GetField("lastName").StringValue);
    Console.WriteLine(row.GetField("age").IntValue);
}
```

------
#### [ Sync ]

```
Console.WriteLine("Querying the table");

IIonValue ionFirstName = valueFactory.NewString("John");

// The result from driver.Execute() is buffered into memory because once the
// transaction is committed, streaming the result is no longer possible.
IResult selectResult = driver.Execute(txn =>
{
    return txn.Execute("SELECT * FROM Person WHERE firstName = ?", ionFirstName);
});

foreach (IIonValue row in selectResult)
{
    Console.WriteLine(row.GetField("firstName").StringValue);
    Console.WriteLine(row.GetField("lastName").StringValue);
    Console.WriteLine(row.GetField("age").IntValue);
}
```

------

此示例使用问号（`?`）作为变量占位符，将文档信息传递给语句。使用占位符时，必须传递一个 `IonValue` 类型的值。

## 第 6 步：更新文档
<a name="driver-quickstart-dotnet.step-6"></a>

以下代码示例显示如何运行 `UPDATE` 语句。

1. 添加以下代码，通过更新 `age` 到 42 来更新`Person`表格中的文档。

------
#### [ Async ]

   ```
   Console.WriteLine("Updating the document");
   
   await driver.Execute(async txn =>
   {
       IQuery<Person> myQuery = txn.Query<Person>("UPDATE Person SET Age = ? WHERE FirstName = ?", 42, "John");
       await txn.Execute(myQuery);
   });
   ```

------
#### [ Sync ]

   ```
   Console.WriteLine("Updating the document");
   
   driver.Execute(txn =>
   {
       IQuery<Person> myQuery = txn.Query<Person>("UPDATE Person SET Age = ? WHERE FirstName = ?", 42, "John");
       txn.Execute(myQuery);
   });
   ```

------

1. 再次查询文档以查看更新的值。

------
#### [ Async ]

   ```
   Console.WriteLine("Querying the table for the updated document");
   
   IAsyncResult<Person> updateResult = await driver.Execute(async txn =>
   {
       IQuery<Person> myQuery = txn.Query<Person>("SELECT * FROM Person WHERE FirstName = ?", "John");
       return await txn.Execute(myQuery);
   });
   
   await foreach (Person person in updateResult)
   {
       Console.WriteLine(person);
       // John, Doe, 42
   }
   ```

------
#### [ Sync ]

   ```
   Console.WriteLine("Querying the table for the updated document");
   
   IResult<Person> updateResult = driver.Execute(txn =>
   {
       IQuery<Person> myQuery = txn.Query<Person>("SELECT * FROM Person WHERE FirstName = ?", "John");
       return txn.Execute(myQuery);
   });
   
   foreach (Person person in updateResult)
   {
       Console.WriteLine(person);
       // John, Doe, 42
   }
   ```

------

1. 要运行该应用程序，请在 `Amazon.QLDB.QuickStartGuide` 项目目录的父目录中输入以下命令。

   ```
   $ dotnet run --project Amazon.QLDB.QuickStartGuide
   ```

### 使用 Ion 库
<a name="driver-quickstart-dotnet.step-6.ion-library"></a>

1. 添加以下代码，通过更新 `age` 到 42 来更新`Person`表格中的文档。

------
#### [ Async ]

   ```
   Console.WriteLine("Updating the document");
   
   IIonValue ionIntAge = valueFactory.NewInt(42);
   IIonValue ionFirstName2 = valueFactory.NewString("John");
   
   await driver.Execute(async txn =>
   {
       await txn.Execute("UPDATE Person SET age = ? WHERE firstName = ?", ionIntAge, ionFirstName2);
   });
   ```

------
#### [ Sync ]

   ```
   Console.WriteLine("Updating a document");
   
   IIonValue ionIntAge = valueFactory.NewInt(42);
   IIonValue ionFirstName2 = valueFactory.NewString("John");
   
   driver.Execute(txn =>
   {
       txn.Execute("UPDATE Person SET age = ? WHERE firstName = ?", ionIntAge, ionFirstName2);
   });
   ```

------

1. 再次查询文档以查看更新的值。

------
#### [ Async ]

   ```
   Console.WriteLine("Querying the table for the updated document");
   
   IIonValue ionFirstName3 = valueFactory.NewString("John");
   
   IAsyncResult updateResult = await driver.Execute(async txn =>
   {
       return await txn.Execute("SELECT * FROM Person WHERE firstName = ?", ionFirstName3 );
   });
   
   await foreach (IIonValue row in updateResult)
   {
       Console.WriteLine(row.GetField("firstName").StringValue);
       Console.WriteLine(row.GetField("lastName").StringValue);
       Console.WriteLine(row.GetField("age").IntValue);
   }
   ```

------
#### [ Sync ]

   ```
   Console.WriteLine("Querying the table for the updated document");
   
   IIonValue ionFirstName3 = valueFactory.NewString("John");
   
   IResult updateResult = driver.Execute(txn =>
   {
       return txn.Execute("SELECT * FROM Person WHERE firstName = ?", ionFirstName3);
   });
   
   foreach (IIonValue row in updateResult)
   {
       Console.WriteLine(row.GetField("firstName").StringValue);
       Console.WriteLine(row.GetField("lastName").StringValue);
       Console.WriteLine(row.GetField("age").IntValue);
   }
   ```

------

1. 要运行该应用程序，请在 `Amazon.QLDB.QuickStartGuide` 项目目录的父目录中输入以下命令。

   ```
   $ dotnet run --project Amazon.QLDB.QuickStartGuide
   ```

## 运行完整的应用程序
<a name="driver-quickstart-dotnet.complete"></a>

以下代码示例是 `Program.cs` 应用程序的完整版本。您还可以从头到尾复制并运行此代码示例，而不必单独执行前面的步骤。此应用程序演示了对名为 `quick-start` 的分类账的一些基本的 CRUD 操作。

**注意**  
在运行此代码之前，请确保 `quick-start` 分类账中还没有名为 `Person` 的活动表。

------
#### [ Async ]

```
using Amazon.QLDB.Driver;
using Amazon.QLDB.Driver.Generic;
using Amazon.QLDB.Driver.Serialization;

namespace Amazon.QLDB.QuickStartGuide
{
    class Program
    {
        public class Person
        {
            public string FirstName { get; set; }

            public string LastName { get; set; }

            public int Age { get; set; }

            public override string ToString()
            {
                return FirstName + ", " + LastName + ", " + Age.ToString();
            }
        }

        static async Task Main(string[] args)
        {
            Console.WriteLine("Create the async QLDB driver");
            IAsyncQldbDriver driver = AsyncQldbDriver.Builder()
                .WithLedger("quick-start")
                .WithSerializer(new ObjectSerializer())
                .Build();

            Console.WriteLine("Creating the table and index");

            // Creates the table and the index in the same transaction.
            // Note: Any code within the lambda can potentially execute multiple times due to retries.
            // For more information, see: https://docs.aws.amazon.com/qldb/latest/developerguide/driver-retry-policy
            await driver.Execute(async txn =>
            {
                await txn.Execute("CREATE TABLE Person");
                await txn.Execute("CREATE INDEX ON Person(firstName)");
            });

            Console.WriteLine("Inserting a document");

            Person myPerson = new Person {
                FirstName = "John",
                LastName = "Doe",
                Age = 32
            };

            await driver.Execute(async txn =>
            {
                IQuery<Person> myQuery = txn.Query<Person>("INSERT INTO Person ?", myPerson);
                await txn.Execute(myQuery);
            });

            Console.WriteLine("Querying the table");

            // The result from driver.Execute() is buffered into memory because once the
            // transaction is committed, streaming the result is no longer possible.
            IAsyncResult<Person> selectResult = await driver.Execute(async txn =>
            {
                IQuery<Person> myQuery = txn.Query<Person>("SELECT * FROM Person WHERE FirstName = ?", "John");
                return await txn.Execute(myQuery);
            });

            await foreach (Person person in selectResult)
            {
                Console.WriteLine(person);
                // John, Doe, 32
            }

            Console.WriteLine("Updating the document");

            await driver.Execute(async txn =>
            {
                IQuery<Person> myQuery = txn.Query<Person>("UPDATE Person SET Age = ? WHERE FirstName = ?", 42, "John");
                await txn.Execute(myQuery);
            });

            Console.WriteLine("Querying the table for the updated document");

            IAsyncResult<Person> updateResult = await driver.Execute(async txn =>
            {
                IQuery<Person> myQuery = txn.Query<Person>("SELECT * FROM Person WHERE FirstName = ?", "John");
                return await txn.Execute(myQuery);
            });

            await foreach (Person person in updateResult)
            {
                Console.WriteLine(person);
                // John, Doe, 42
            }
        }
    }
}
```

------
#### [ Sync ]

```
using Amazon.QLDB.Driver;
using Amazon.QLDB.Driver.Generic;
using Amazon.QLDB.Driver.Serialization;

namespace Amazon.QLDB.QuickStartGuide
{
    class Program
    {
        public class Person
        {
            public string FirstName { get; set; }

            public string LastName { get; set; }

            public int Age { get; set; }

            public override string ToString()
            {
                return FirstName + ", " + LastName + ", " + Age.ToString();
            }
        }

        static void Main(string[] args)
        {
            Console.WriteLine("Create the sync QLDB driver");
            IQldbDriver driver = QldbDriver.Builder()
                .WithLedger("quick-start")
                .WithSerializer(new ObjectSerializer())
                .Build();

            Console.WriteLine("Creating the table and index");

            // Creates the table and the index in the same transaction.
            // Note: Any code within the lambda can potentially execute multiple times due to retries.
            // For more information, see: https://docs.aws.amazon.com/qldb/latest/developerguide/driver-retry-policy
            driver.Execute(txn =>
            {
                txn.Execute("CREATE TABLE Person");
                txn.Execute("CREATE INDEX ON Person(firstName)");
            });

            Console.WriteLine("Inserting a document");

            Person myPerson = new Person {
                FirstName = "John",
                LastName = "Doe",
                Age = 32
            };

            driver.Execute(txn =>
            {
                IQuery<Person> myQuery = txn.Query<Person>("INSERT INTO Person ?", myPerson);
                txn.Execute(myQuery);
            });

            Console.WriteLine("Querying the table");

            // The result from driver.Execute() is buffered into memory because once the
            // transaction is committed, streaming the result is no longer possible.
            IResult<Person> selectResult = driver.Execute(txn =>
            {
                IQuery<Person> myQuery = txn.Query<Person>("SELECT * FROM Person WHERE FirstName = ?", "John");
                return txn.Execute(myQuery);
            });

            foreach (Person person in selectResult)
            {
                Console.WriteLine(person);
                // John, Doe, 32
            }

            Console.WriteLine("Updating the document");

            driver.Execute(txn =>
            {
                IQuery<Person> myQuery = txn.Query<Person>("UPDATE Person SET Age = ? WHERE FirstName = ?", 42, "John");
                txn.Execute(myQuery);
            });

            Console.WriteLine("Querying the table for the updated document");

            IResult<Person> updateResult = driver.Execute(txn =>
            {
                IQuery<Person> myQuery = txn.Query<Person>("SELECT * FROM Person WHERE FirstName = ?", "John");
                return txn.Execute(myQuery);
            });

            foreach (Person person in updateResult)
            {
                Console.WriteLine(person);
                // John, Doe, 42
            }

        }
    }
}
```

------

### 使用 Ion 库
<a name="driver-quickstart-dotnet.complete.ion-library"></a>

------
#### [ Async ]

```
using System;
using System.Threading.Tasks;
using Amazon.IonDotnet.Tree;
using Amazon.IonDotnet.Tree.Impl;
using Amazon.QLDB.Driver;
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;

namespace Amazon.QLDB.QuickStartGuide
{
    class Program
    {
        static IValueFactory valueFactory = new ValueFactory();

        static async Task Main(string[] args)
        {
            Console.WriteLine("Create the async QLDB driver");
            IAsyncQldbDriver driver = AsyncQldbDriver.Builder()
                .WithLedger("quick-start")
                .Build();

            Console.WriteLine("Creating the table and index");

            // Creates the table and the index in the same transaction.
            // Note: Any code within the lambda can potentially execute multiple times due to retries.
            // For more information, see: https://docs.aws.amazon.com/qldb/latest/developerguide/driver-retry-policy
            await driver.Execute(async txn =>
            {
                await txn.Execute("CREATE TABLE Person");
                await txn.Execute("CREATE INDEX ON Person(firstName)");
            });

            Console.WriteLine("Inserting a document");

            // This is one way of creating Ion values. We can also use an IonLoader.
            // For more details, see: https://docs.aws.amazon.com/qldb/latest/developerguide/driver-cookbook-dotnet.html#cookbook-dotnet.ion
            IIonValue ionPerson = valueFactory.NewEmptyStruct();
            ionPerson.SetField("firstName", valueFactory.NewString("John"));
            ionPerson.SetField("lastName", valueFactory.NewString("Doe"));
            ionPerson.SetField("age", valueFactory.NewInt(32));

            await driver.Execute(async txn =>
            {
                await txn.Execute("INSERT INTO Person ?", ionPerson);
            });

            Console.WriteLine("Querying the table");

            IIonValue ionFirstName = valueFactory.NewString("John");

            // The result from driver.Execute() is buffered into memory because once the
            // transaction is committed, streaming the result is no longer possible.
            IAsyncResult selectResult = await driver.Execute(async txn =>
            {
                return await txn.Execute("SELECT * FROM Person WHERE firstName = ?", ionFirstName);
            });

            await foreach (IIonValue row in selectResult)
            {
                Console.WriteLine(row.GetField("firstName").StringValue);
                Console.WriteLine(row.GetField("lastName").StringValue);
                Console.WriteLine(row.GetField("age").IntValue);
            }

            Console.WriteLine("Updating the document");

            IIonValue ionIntAge = valueFactory.NewInt(42);
            IIonValue ionFirstName2 = valueFactory.NewString("John");

            await driver.Execute(async txn =>
            {
                await txn.Execute("UPDATE Person SET age = ? WHERE firstName = ?", ionIntAge, ionFirstName2);
            });

            Console.WriteLine("Querying the table for the updated document");

            IIonValue ionFirstName3 = valueFactory.NewString("John");

            IAsyncResult updateResult = await driver.Execute(async txn =>
            {
                return await txn.Execute("SELECT * FROM Person WHERE firstName = ?", ionFirstName3);
            });

            await foreach (IIonValue row in updateResult)
            {
                Console.WriteLine(row.GetField("firstName").StringValue);
                Console.WriteLine(row.GetField("lastName").StringValue);
                Console.WriteLine(row.GetField("age").IntValue);
            }
        }
    }
}
```

------
#### [ Sync ]

```
using System;
using Amazon.IonDotnet.Tree;
using Amazon.IonDotnet.Tree.Impl;
using Amazon.QLDB.Driver;

namespace Amazon.QLDB.QuickStartGuide
{
    class Program
    {
        static IValueFactory valueFactory = new ValueFactory();

        static void Main(string[] args)
        {
            Console.WriteLine("Create the sync QLDB Driver");
            IQldbDriver driver = QldbDriver.Builder()
                .WithLedger("quick-start")
                .Build();

            Console.WriteLine("Creating the tables and index");

            // Creates the table and the index in the same transaction.
            // Note: Any code within the lambda can potentially execute multiple times due to retries.
            // For more information, see: https://docs.aws.amazon.com/qldb/latest/developerguide/driver-retry-policy
            driver.Execute(txn =>
            {
                txn.Execute("CREATE TABLE Person");
                txn.Execute("CREATE INDEX ON Person(firstName)");
            });

            Console.WriteLine("Inserting a document");

            // This is one way of creating Ion values. We can also use an IonLoader.
            // For more details, see: https://docs.aws.amazon.com/qldb/latest/developerguide/driver-cookbook-dotnet.html#cookbook-dotnet.ion
            IIonValue ionPerson = valueFactory.NewEmptyStruct();
            ionPerson.SetField("firstName", valueFactory.NewString("John"));
            ionPerson.SetField("lastName", valueFactory.NewString("Doe"));
            ionPerson.SetField("age", valueFactory.NewInt(32));

            driver.Execute(txn =>
            {
                txn.Execute("INSERT INTO Person ?", ionPerson);
            });

            Console.WriteLine("Querying the table");

            IIonValue ionFirstName = valueFactory.NewString("John");

            // The result from driver.Execute() is buffered into memory because once the
            // transaction is committed, streaming the result is no longer possible.
            IResult selectResult = driver.Execute(txn =>
            {
                return txn.Execute("SELECT * FROM Person WHERE firstName = ?", ionFirstName);
            });

            foreach (IIonValue row in selectResult)
            {
                Console.WriteLine(row.GetField("firstName").StringValue);
                Console.WriteLine(row.GetField("lastName").StringValue);
                Console.WriteLine(row.GetField("age").IntValue);
            }

            Console.WriteLine("Updating a document");

            IIonValue ionIntAge = valueFactory.NewInt(42);
            IIonValue ionFirstName2 = valueFactory.NewString("John");

            driver.Execute(txn =>
            {
                txn.Execute("UPDATE Person SET age = ? WHERE firstName = ?", ionIntAge, ionFirstName2);
            });

            Console.WriteLine("Querying the table for the updated document");

            IIonValue ionFirstName3 = valueFactory.NewString("John");

            IResult updateResult = driver.Execute(txn =>
            {
                return txn.Execute("SELECT * FROM Person WHERE firstName = ?", ionFirstName3);
            });

            foreach (IIonValue row in updateResult)
            {
                Console.WriteLine(row.GetField("firstName").StringValue);
                Console.WriteLine(row.GetField("lastName").StringValue);
                Console.WriteLine(row.GetField("age").IntValue);
            }
        }
    }
}
```

------

要运行该应用程序，请在 `Amazon.QLDB.QuickStartGuide` 项目目录的父目录中输入以下命令。

```
$ dotnet run --project Amazon.QLDB.QuickStartGuide
```

# 适用于 .NET 的 Amazon QLDB 驱动程序 — 说明书参考
<a name="driver-cookbook-dotnet"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

本参考指南显示了 .NET 的 Amazon QLDB 驱动程序的常见用例。它提供了 C\$1 代码示例，演示了如何使用该驱动程序运行基本的*创建、读取、更新和删除*（CRUD）操作。它还包括用于处理 Amazon Ion 数据的代码示例。此外，本指南还重点介绍了使事务具有幂等性和实现唯一性约束的最佳实践。

**注意**  
本主题提供了默认使用 [Ion 对象映射器](https://github.com/amzn/ion-object-mapper-dotnet) 处理 Amazon Ion 的代码示例。QLDB 在.NET 驱动程序的 1.3.0 版本中引入 Ion 对象映射器。在适用的情况下，本主题还提供了使用标准 [Ion 库](https://github.com/amzn/ion-dotnet) 作为替代方案代码示例。要了解更多信息，请参阅 [使用 Amazon Ion](#cookbook-dotnet.ion)。

**Contents**
+ [

## 导入驱动程序
](#cookbook-dotnet.importing)
+ [

## 实例化驱动程序
](#cookbook-dotnet.instantiating)
+ [

## CRUD 操作
](#cookbook-dotnet.crud)
  + [

### 创建表
](#cookbook-dotnet.crud.creating-tables)
  + [

### 创建索引
](#cookbook-dotnet.crud.creating-indexes)
  + [

### 阅读文档
](#cookbook-dotnet.crud.reading)
    + [

#### 使用查询参数
](#cookbook-dotnet.reading-using-params)
  + [

### 插入文档
](#cookbook-dotnet.crud.inserting)
    + [

#### 在一条语句内插入多个文档
](#cookbook-dotnet.crud.inserting.multiple)
  + [

### 更新文档
](#cookbook-dotnet.crud.updating)
  + [

### 删除文档
](#cookbook-dotnet.crud.deleting)
  + [

### 在一个事务中运行多条语句
](#cookbook-dotnet.crud.multi-statement)
  + [

### 重试逻辑
](#cookbook-dotnet.crud.retry-logic)
  + [

### 实现唯一限制
](#cookbook-dotnet.crud.uniqueness-constraints)
+ [

## 使用 Amazon Ion
](#cookbook-dotnet.ion)
  + [

### 导入 Ion 模块
](#cookbook-dotnet.ion.import)
  + [

### 创建 Ion 类型
](#cookbook-dotnet.ion.creating-types)
  + [

### 获取 Ion 二进制转储
](#cookbook-dotnet.ion.getting-binary)
  + [

### 获取 Ion 文本转储
](#cookbook-dotnet.ion.getting-text)

## 导入驱动程序
<a name="cookbook-dotnet.importing"></a>

下面的代码示例导入驱动程序。

```
using Amazon.QLDB.Driver;
using Amazon.QLDB.Driver.Generic;
using Amazon.QLDB.Driver.Serialization;
```

### 使用 Ion 库
<a name="cookbook-dotnet.importing.ion-library"></a>

```
using Amazon.QLDB.Driver;
using Amazon.IonDotnet.Builders;
```

## 实例化驱动程序
<a name="cookbook-dotnet.instantiating"></a>

以下代码示例使用默认设置创建连接到指定分类账名称的驱动程序实例。

------
#### [ Async ]

```
IAsyncQldbDriver driver = AsyncQldbDriver.Builder()
    .WithLedger("vehicle-registration")
    // Add Serialization library
    .WithSerializer(new ObjectSerializer())
    .Build();
```

------
#### [ Sync ]

```
IQldbDriver driver = QldbDriver.Builder()
    .WithLedger("vehicle-registration")
    // Add Serialization library
    .WithSerializer(new ObjectSerializer())
    .Build();
```

------

### 使用 Ion 库
<a name="cookbook-dotnet.instantiating.ion-library"></a>

------
#### [ Async ]

```
IAsyncQldbDriver driver = AsyncQldbDriver.Builder().WithLedger("vehicle-registration").Build();
```

------
#### [ Sync ]

```
IQldbDriver driver = QldbDriver.Builder().WithLedger("vehicle-registration").Build();
```

------

## CRUD 操作
<a name="cookbook-dotnet.crud"></a>

QLDB 作为事务的一部分运行*创建、读取、更新和删除*（CRUD）操作。

**警告**  
作为最佳实践，使写事务严格地幂等。

**使事务幂等**

我们建议将写事务设置为幂等，以避免重试时出现任何意想不到的副作用。如果事务可以运行多次并每次都产生相同的结果，则事务是*幂等的*。

例如，假设有一个事务，要将文档插入名为 `Person` 的表中。事务应该首先检查文档是否已经存在于表中。如果没有这种检查，表最终可能会有重复的文档。

假设 QLDB 在服务器端成功提交了事务，但客户端在等待响应时超时。如果事务不是幂等的，则在重试的情况下可以多次插入相同的文档。

**使用索引避免全表扫描**

我们还建议您在索引字段或文档 ID 上使用*相等*运算符来运行带有`WHERE`谓词子句的语句；例如，`WHERE indexedField = 123`或 `WHERE indexedField IN (456, 789)`。如果没有这种索引查找，QLDB 需要进行表扫描，这可能会导致事务超时或*乐观并发控制*（OCC）冲突。

有关 OCC 的更多信息，请参阅[Amazon QLDB 并发模型](concurrency.md)。

**隐式创建的事务**

[Amazon.qldb.Driver。 ](https://amazon-qldb-docs.s3.amazonaws.com/drivers/dotnet/1.4.1/api/Amazon.QLDB.Driver.IQldbDriver.html)IQldb[Driver.Execute 方法接受接收 Amazon.qldb.Driver 实例的 lambda 函数。 TransactionExecutor](https://amazon-qldb-docs.s3.amazonaws.com/drivers/dotnet/1.4.1/api/Amazon.QLDB.Driver.TransactionExecutor.html)，你可以用它来运行语句。`TransactionExecutor` 的实例封装了隐式创建的事务。

您可以使用事务执行器的 `Execute` 执行方法在 lambda 函数中运行语句。当 lambda 函数返回时，驱动程序会隐式提交事务。

以下各节介绍如何运行基本的 CRUD 操作、指定自定义重试逻辑以及如何实现唯一性约束。

**Contents**
+ [

### 创建表
](#cookbook-dotnet.crud.creating-tables)
+ [

### 创建索引
](#cookbook-dotnet.crud.creating-indexes)
+ [

### 阅读文档
](#cookbook-dotnet.crud.reading)
  + [

#### 使用查询参数
](#cookbook-dotnet.reading-using-params)
+ [

### 插入文档
](#cookbook-dotnet.crud.inserting)
  + [

#### 在一条语句内插入多个文档
](#cookbook-dotnet.crud.inserting.multiple)
+ [

### 更新文档
](#cookbook-dotnet.crud.updating)
+ [

### 删除文档
](#cookbook-dotnet.crud.deleting)
+ [

### 在一个事务中运行多条语句
](#cookbook-dotnet.crud.multi-statement)
+ [

### 重试逻辑
](#cookbook-dotnet.crud.retry-logic)
+ [

### 实现唯一限制
](#cookbook-dotnet.crud.uniqueness-constraints)

### 创建表
<a name="cookbook-dotnet.crud.creating-tables"></a>

------
#### [ Async ]

```
IAsyncResult<Table> createResult = await driver.Execute(async txn =>
{
    IQuery<Table> query = txn.Query<Table>("CREATE TABLE Person");
    return await txn.Execute(query);
});

await foreach (var result in createResult)
{
    Console.WriteLine("{ tableId: " + result.TableId + " }");
    // The statement returns the created table ID:
    // { tableId: 4o5Uk09OcjC6PpJpLahceE }
}
```

------
#### [ Sync ]

```
IResult<Table> createResult = driver.Execute( txn =>
{
    IQuery<Table> query = txn.Query<Table>("CREATE TABLE Person");
    return txn.Execute(query);
});

foreach (var result in createResult)
{
    Console.WriteLine("{ tableId: " + result.TableId + " }");
    // The statement returns the created table ID:
    // { tableId: 4o5Uk09OcjC6PpJpLahceE }
}
```

------

#### 使用 Ion 库
<a name="cookbook-dotnet.creating-tables.ion-library"></a>

------
#### [ Async ]

```
// The result from driver.Execute() is buffered into memory because once the
// transaction is committed, streaming the result is no longer possible.
IAsyncResult result = await driver.Execute(async txn =>
{
    return await txn.Execute("CREATE TABLE Person");
});

await foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // The statement returns the created table ID:
    // {
    //    tableId: "4o5Uk09OcjC6PpJpLahceE"
    // }
}
```

------
#### [ Sync ]

```
// The result from driver.Execute() is buffered into memory because once the
// transaction is committed, streaming the result is no longer possible.
IResult result = driver.Execute(txn =>
{
    return txn.Execute("CREATE TABLE Person");
});

foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // The statement returns the created table ID:
    // {
    //    tableId: "4o5Uk09OcjC6PpJpLahceE"
    // }
}
```

------

### 创建索引
<a name="cookbook-dotnet.crud.creating-indexes"></a>

------
#### [ Async ]

```
IAsyncResult<Table> createResult = await driver.Execute(async txn =>
{
    IQuery<Table> query = txn.Query<Table>("CREATE INDEX ON Person(firstName)");
    return await txn.Execute(query);
});

await foreach (var result in createResult)
{
    Console.WriteLine("{ tableId: " + result.TableId + " }");
    // The statement returns the updated table ID:
    // { tableId: 4o5Uk09OcjC6PpJpLahceE }
}
```

------
#### [ Sync ]

```
IResult<Table> createResult = driver.Execute(txn =>
{
    IQuery<Table> query = txn.Query<Table>("CREATE INDEX ON Person(firstName)");
    return txn.Execute(query);
});

foreach (var result in createResult)
{
    Console.WriteLine("{ tableId: " + result.TableId + " }");
    // The statement returns the updated table ID:
    // { tableId: 4o5Uk09OcjC6PpJpLahceE }
}
```

------

#### 使用 Ion 库
<a name="cookbook-dotnet.creating-indexes.ion-library"></a>

------
#### [ Async ]

```
IAsyncResult result = await driver.Execute(async txn =>
{
    return await txn.Execute("CREATE INDEX ON Person(GovId)");
});

await foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // The statement returns the updated table ID:
    // {
    //    tableId: "4o5Uk09OcjC6PpJpLahceE"
    // }
}
```

------
#### [ Sync ]

```
IResult result = driver.Execute(txn =>
{
    return txn.Execute("CREATE INDEX ON Person(GovId)");
});

foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // The statement returns the updated table ID:
    // {
    //    tableId: "4o5Uk09OcjC6PpJpLahceE"
    // }
}
```

------

### 阅读文档
<a name="cookbook-dotnet.crud.reading"></a>

```
// Assumes that Person table has documents as follows:
// { "GovId": "TOYENC486FH", "FirstName" : "Brent" }
// Person class is defined as follows:
// public class Person
// {
//     public string GovId { get; set; }
//     public string FirstName { get; set; }
//  }

IAsyncResult<Person> result = await driver.Execute(async txn =>
{
    return await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE GovId = 'TOYENC486FH'"));
});

await foreach (Person person in result)
{
    Console.WriteLine(person.GovId); // Prints TOYENC486FH.
    Console.WriteLine(person.FirstName); // Prints Brent.
}
```

**注意**  
当您在没有索引查找的情况下运行查询时，它会调用全表扫描。在此示例中，我们建议在`GovId`字段上设置 [索引](ql-reference.create-index.md)以优化性能。如果不开启`GovId`索引，查询可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

#### 使用查询参数
<a name="cookbook-dotnet.reading-using-params"></a>

以下代码示例使用 C\$1 类型查询参数。

```
IAsyncResult<Person> result = await driver.Execute(async txn =>
{
    return await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE FirstName = ?", "Brent"));
});

await foreach (Person person in result)
{
    Console.WriteLine(person.GovId); // Prints TOYENC486FH.
    Console.WriteLine(person.FirstName); // Prints Brent.
}
```

以下代码示例使用了多个 C\$1 类型查询参数。

```
IAsyncResult<Person> result = await driver.Execute(async txn =>
{
    return await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE GovId = ? AND FirstName = ?", "TOYENC486FH", "Brent"));
});

await foreach (Person person in result)
{
    Console.WriteLine(person.GovId); // Prints TOYENC486FH.
    Console.WriteLine(person.FirstName); // Prints Brent.
}
```

以下代码示例使用 C\$1 类型查询参数。

```
// Assumes that Person table has documents as follows:
// { "GovId": "TOYENC486FH", "FirstName" : "Brent" }
// { "GovId": "ROEE1C1AABH", "FirstName" : "Jim" }
// { "GovId": "YH844DA7LDB", "FirstName" : "Mary" }

string[] ids = {
    "TOYENC486FH",
    "ROEE1C1AABH",
    "YH844DA7LDB"
};

IAsyncResult<Person> result = await driver.Execute(async txn =>
{
    return await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE GovId IN (?,?,?)", ids));
});

await foreach (Person person in result)
{
    Console.WriteLine(person.FirstName); // Prints Brent on first iteration.
    // Prints Jim on second iteration.
    // Prints Mary on third iteration.
}
```

以下代码示例使用 C\$1 列表为值。

```
// Assumes that Person table has document as follows:
// { "GovId": "TOYENC486FH",
//   "FirstName" : "Brent",
//   "Vehicles": [
//      { "Make": "Volkswagen",
//        "Model": "Golf"},
//      { "Make": "Honda",
//        "Model": "Civic"}
//   ]
// }
// Person class is defined as follows:
// public class Person
// {
//     public string GovId { get; set; }
//     public string FirstName { get; set; }
//     public List<Vehicle> Vehicles { get; set; }
// }
// Vehicle class is defined as follows:
// public class Vehicle
// {
//     public string Make { get; set; }
//     public string Model { get; set; }
// }

List<Vehicle> vehicles = new List<Vehicle>
{
    new Vehicle
    {
        Make = "Volkswagen",
        Model = "Golf"
    },
    new Vehicle
    {
        Make = "Honda",
        Model = "Civic"
    }
};

IAsyncResult<Person> result = await driver.Execute(async txn =>
{
    return await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE Vehicles = ?", vehicles));
});

await foreach (Person person in result)
{
    Console.WriteLine("{");
    Console.WriteLine($"  GovId: {person.GovId},");
    Console.WriteLine($"  FirstName: {person.FirstName},");
    Console.WriteLine("  Vehicles: [");
    foreach (Vehicle vehicle in person.Vehicles)
    {
        Console.WriteLine("  {");
        Console.WriteLine($"    Make: {vehicle.Make},");
        Console.WriteLine($"    Model: {vehicle.Model},");
        Console.WriteLine("  },");
    }
    Console.WriteLine("  ]");
    Console.WriteLine("}");
    // Prints:
    // {
    //     GovId: TOYENC486FH,
    //     FirstName: Brent,
    //     Vehicles: [
    //     {
    //         Make: Volkswagen,
    //         Model: Golf
    //     },
    //     {
    //         Make: Honda,
    //         Model: Civic
    //     },
    //     ]
    // }
}
```

#### 使用 Ion 库
<a name="cookbook-dotnet.crud.reading.ion-library"></a>

------
#### [ Async ]

```
// Assumes that Person table has documents as follows:
// { "GovId": "TOYENC486FH", "FirstName" : "Brent" }

IAsyncResult result = await driver.Execute(async txn =>
{
    return await txn.Execute("SELECT * FROM Person WHERE GovId = 'TOYENC486FH'");
});

await foreach (IIonValue row in result)
{
    Console.WriteLine(row.GetField("GovId").StringValue); // Prints TOYENC486FH.
    Console.WriteLine(row.GetField("FirstName").StringValue); // Prints Brent.
}
```

------
#### [ Sync ]

```
// Assumes that Person table has documents as follows:
// { "GovId": "TOYENC486FH", "FirstName" : "Brent" }

IResult result = driver.Execute(txn =>
{
    return txn.Execute("SELECT * FROM Person WHERE GovId = 'TOYENC486FH'");
});

foreach (IIonValue row in result)
{
    Console.WriteLine(row.GetField("GovId").StringValue); // Prints TOYENC486FH.
    Console.WriteLine(row.GetField("FirstName").StringValue); // Prints Brent.
}
```

------

**注意**  
当您在没有索引查找的情况下运行查询时，它会调用全表扫描。在此示例中，我们建议在`GovId`字段上设置 [索引](ql-reference.create-index.md)以优化性能。如果不开启`GovId`索引，查询可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

以下代码示例使用 Ion 类型查询参数。

------
#### [ Async ]

```
IValueFactory valueFactory = new ValueFactory();
IIonValue ionFirstName = valueFactory.NewString("Brent");

IAsyncResult result = await driver.Execute(async txn =>
{
    return await txn.Execute("SELECT * FROM Person WHERE FirstName = ?", ionFirstName);
});

await foreach (IIonValue row in result)
{
    Console.WriteLine(row.GetField("GovId").StringValue); // Prints TOYENC486FH.
    Console.WriteLine(row.GetField("FirstName").StringValue); // Prints Brent.
}
```

------
#### [ Sync ]

```
IValueFactory valueFactory = new ValueFactory();
IIonValue ionFirstName = valueFactory.NewString("Brent");

IResult result = driver.Execute(txn =>
{
    return txn.Execute("SELECT * FROM Person WHERE FirstName = ?", ionFirstName);
});

foreach (IIonValue row in result)
{
    Console.WriteLine(row.GetField("GovId").StringValue); // Prints TOYENC486FH.
    Console.WriteLine(row.GetField("FirstName").StringValue); // Prints Brent.
}
```

------

以下代码示例使用了多个查询参数。

------
#### [ Async ]

```
IIonValue ionGovId = valueFactory.NewString("TOYENC486FH");
IIonValue ionFirstName = valueFactory.NewString("Brent");

IAsyncResult result = await driver.Execute(async txn =>
{
    return await txn.Execute("SELECT * FROM Person WHERE GovId = ? AND FirstName = ?", ionGovId, ionFirstName);
});

await foreach (IIonValue row in result)
{
    Console.WriteLine(row.GetField("GovId").StringValue); // Prints TOYENC486FH.
    Console.WriteLine(row.GetField("FirstName").StringValue); // Prints Brent.
}
```

------
#### [ Sync ]

```
IIonValue ionGovId = valueFactory.NewString("TOYENC486FH");
IIonValue ionFirstName = valueFactory.NewString("Brent");

IResult result = driver.Execute(txn =>
{
    return txn.Execute("SELECT * FROM Person WHERE GovId = ? AND FirstName = ?", ionGovId, ionFirstName);
});

foreach (IIonValue row in result)
{
    Console.WriteLine(row.GetField("GovId").StringValue); // Prints TOYENC486FH.
    Console.WriteLine(row.GetField("FirstName").StringValue); // Prints Brent.
}
```

------

以下代码示例使用查询参数列表。

------
#### [ Async ]

```
// Assumes that Person table has documents as follows:
// { "GovId": "TOYENC486FH", "FirstName" : "Brent" }
// { "GovId": "ROEE1C1AABH", "FirstName" : "Jim" }
// { "GovId": "YH844DA7LDB", "FirstName" : "Mary" }

IIonValue[] ionIds = {
    valueFactory.NewString("TOYENC486FH"),
    valueFactory.NewString("ROEE1C1AABH"),
    valueFactory.NewString("YH844DA7LDB")
};

IAsyncResult result = await driver.Execute(async txn =>
{
    return await txn.Execute("SELECT * FROM Person WHERE GovId IN (?,?,?)", ionIds);
});

await foreach (IIonValue row in result)
{
    Console.WriteLine(row.GetField("FirstName").StringValue); // Prints Brent on first iteration.
                                                              // Prints Jim on second iteration.
                                                              // Prints Mary on third iteration.
}
```

------
#### [ Sync ]

```
// Assumes that Person table has documents as follows:
// { "GovId": "TOYENC486FH", "FirstName" : "Brent" }
// { "GovId": "ROEE1C1AABH", "FirstName" : "Jim" }
// { "GovId": "YH844DA7LDB", "FirstName" : "Mary" }

IIonValue[] ionIds = {
    valueFactory.NewString("TOYENC486FH"),
    valueFactory.NewString("ROEE1C1AABH"),
    valueFactory.NewString("YH844DA7LDB")
};

IResult result = driver.Execute(txn =>
{
    return txn.Execute("SELECT * FROM Person WHERE GovId IN (?,?,?)", ionIds);
});

foreach (IIonValue row in result)
{
    Console.WriteLine(row.GetField("FirstName").StringValue); // Prints Brent on first iteration.
                                                              // Prints Jim on second iteration.
                                                              // Prints Mary on third iteration.
}
```

------

以下代码示例使用 C\$1 列表为值。要了解使用不同 Ion 类型任务的更多信息，请参阅 [使用 Amazon QLDB 中的 Amazon Ion 数据类型](driver-working-with-ion.md)。

------
#### [ Async ]

```
// Assumes that Person table has document as follows:
// { "GovId": "TOYENC486FH",
//   "FirstName" : "Brent",
//   "Vehicles": [
//      { "Make": "Volkswagen",
//        "Model": "Golf"},
//      { "Make": "Honda",
//        "Model": "Civic"}
//   ]
// }

IIonValue ionVehicle1 = valueFactory.NewEmptyStruct();
ionVehicle1.SetField("Make", valueFactory.NewString("Volkswagen"));
ionVehicle1.SetField("Model", valueFactory.NewString("Golf"));

IIonValue ionVehicle2 = valueFactory.NewEmptyStruct();
ionVehicle2.SetField("Make", valueFactory.NewString("Honda"));
ionVehicle2.SetField("Model", valueFactory.NewString("Civic"));

IIonValue ionVehicles =  valueFactory.NewEmptyList();
ionVehicles.Add(ionVehicle1);
ionVehicles.Add(ionVehicle2);

IAsyncResult result = await driver.Execute(async txn =>
{
    return await txn.Execute("SELECT * FROM Person WHERE Vehicles = ?", ionVehicles);
});

await foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // Prints:
    // {
    //     GovId: "TOYENC486FN",
    //     FirstName: "Brent",
    //     Vehicles: [
    //     {
    //         Make: "Volkswagen",
    //         Model: "Golf"
    //     },
    //     {
    //         Make: "Honda",
    //         Model: "Civic"
    //     }
    //     ]
    // }
}
```

------
#### [ Sync ]

```
// Assumes that Person table has document as follows:
// { "GovId": "TOYENC486FH",
//   "FirstName" : "Brent",
//   "Vehicles": [
//      { "Make": "Volkswagen",
//        "Model": "Golf"},
//      { "Make": "Honda",
//        "Model": "Civic"}
//   ]
// }

IIonValue ionVehicle1 = valueFactory.NewEmptyStruct();
ionVehicle1.SetField("Make", valueFactory.NewString("Volkswagen"));
ionVehicle1.SetField("Model", valueFactory.NewString("Golf"));

IIonValue ionVehicle2 = valueFactory.NewEmptyStruct();
ionVehicle2.SetField("Make", valueFactory.NewString("Honda"));
ionVehicle2.SetField("Model", valueFactory.NewString("Civic"));

IIonValue ionVehicles =  valueFactory.NewEmptyList();
ionVehicles.Add(ionVehicle1);
ionVehicles.Add(ionVehicle2);

IResult result = driver.Execute(txn =>
{
    return txn.Execute("SELECT * FROM Person WHERE Vehicles = ?", ionVehicles);
});

foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // Prints:
    // {
    //     GovId: "TOYENC486FN",
    //     FirstName: "Brent",
    //     Vehicles: [
    //     {
    //         Make: "Volkswagen",
    //         Model: "Golf"
    //     },
    //     {
    //         Make: "Honda",
    //         Model: "Civic"
    //     }
    //     ]
    // }
}
```

------

### 插入文档
<a name="cookbook-dotnet.crud.inserting"></a>

以下代码示例插入 Ion 数据类型。

```
string govId = "TOYENC486FH";

Person person = new Person
{
    GovId = "TOYENC486FH",
    FirstName = "Brent"
};

await driver.Execute(async txn =>
{
    // Check if a document with GovId:TOYENC486FH exists
    // This is critical to make this transaction idempotent
    IAsyncResult<Person> result = await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE GovId = ?", govId));

    // Check if there is a record in the cursor.
    int count = await result.CountAsync();
    if (count > 0)
    {
        // Document already exists, no need to insert
        return;
    }

    // Insert the document.
    await txn.Execute(txn.Query<Document>("INSERT INTO Person ?", person));
});
```

#### 使用 Ion 库
<a name="cookbook-dotnet.crud.inserting.ion-library"></a>

------
#### [ Async ]

```
IIonValue ionGovId = valueFactory.NewString("TOYENC486FH");

IIonValue ionPerson = valueFactory.NewEmptyStruct();
ionPerson.SetField("GovId", valueFactory.NewString("TOYENC486FH"));
ionPerson.SetField("FirstName", valueFactory.NewString("Brent"));

await driver.Execute(async txn =>
{
    // Check if a document with GovId:TOYENC486FH exists
    // This is critical to make this transaction idempotent
    IAsyncResult result = await txn.Execute("SELECT * FROM Person WHERE GovId = ?", ionGovId);

    // Check if there is a record in the cursor.
    int count = await result.CountAsync();
    if (count > 0)
    {
        // Document already exists, no need to insert
        return;
    }

    // Insert the document.
    await txn.Execute("INSERT INTO Person ?", ionPerson);
});
```

------
#### [ Sync ]

```
IIonValue ionGovId = valueFactory.NewString("TOYENC486FH");

IIonValue ionPerson = valueFactory.NewEmptyStruct();
ionPerson.SetField("GovId", valueFactory.NewString("TOYENC486FH"));
ionPerson.SetField("FirstName", valueFactory.NewString("Brent"));

driver.Execute(txn =>
{
    // Check if a document with GovId:TOYENC486FH exists
    // This is critical to make this transaction idempotent
    IResult result = txn.Execute("SELECT * FROM Person WHERE GovId = ?", ionGovId);

    // Check if there is a record in the cursor.
    int count = result.Count();
    if (count > 0)
    {
        // Document already exists, no need to insert
        return;
    }

    // Insert the document.
    txn.Execute("INSERT INTO Person ?", ionPerson);
});
```

------

此事务将文档插入 `Person` 表中。在插入之前，它首先检查文档是否已存在于表格内。**此检查使事务本质上是幂等。**即使您多次运行此事务，也不会造成任何异常副作用。

**注意**  
在此示例中，我们建议在 `GovId` 字段上设置索引以优化性能。如果不开启`GovId`索引，语句可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

#### 在一条语句内插入多个文档
<a name="cookbook-dotnet.crud.inserting.multiple"></a>

要使用单个 [INSERT](ql-reference.insert.md) 语句插入多个文档，可以按如下方式向该语句传递 C\$1 `List` 参数。

```
Person person1 = new Person
{
    FirstName = "Brent",
    GovId = "TOYENC486FH"
};

Person person2 = new Person
{
    FirstName = "Jim",
    GovId = "ROEE1C1AABH"
};

List<Person> people = new List<Person>();
people.Add(person1);
people.Add(person2);

IAsyncResult<Document> result = await driver.Execute(async txn =>
{
    return await txn.Execute(txn.Query<Document>("INSERT INTO Person ?", people));
});

await foreach (Document row in result)
{
    Console.WriteLine("{ documentId: " + row.DocumentId + " }");
    // The statement returns the created documents' ID:
    // { documentId: 6BFt5eJQDFLBW2aR8LPw42 }
    // { documentId: K5Zrcb6N3gmIEHgGhwoyKF }
}
```

##### 使用 Ion 库
<a name="cookbook-dotnet.crud.inserting.multiple.ion-library"></a>

要使用单个 [INSERT](ql-reference.insert.md) 语句插入多个文档，可以向该语句传递一个[Ion 列表](driver-working-with-ion.md#driver-ion-list)类型的参数，如下所示。

------
#### [ Async ]

```
IIonValue ionPerson1 = valueFactory.NewEmptyStruct();
ionPerson1.SetField("FirstName", valueFactory.NewString("Brent"));
ionPerson1.SetField("GovId", valueFactory.NewString("TOYENC486FH"));

IIonValue ionPerson2 = valueFactory.NewEmptyStruct();
ionPerson2.SetField("FirstName", valueFactory.NewString("Jim"));
ionPerson2.SetField("GovId", valueFactory.NewString("ROEE1C1AABH"));

IIonValue ionPeople = valueFactory.NewEmptyList();
ionPeople.Add(ionPerson1);
ionPeople.Add(ionPerson2);

IAsyncResult result = await driver.Execute(async txn =>
{
    return await txn.Execute("INSERT INTO Person ?", ionPeople);
});

await foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // The statement returns the created documents' ID:
    // {
    //     documentId: "6BFt5eJQDFLBW2aR8LPw42"
    // }
    //
    // {
    //     documentId: "K5Zrcb6N3gmIEHgGhwoyKF"
    // }
}
```

------
#### [ Sync ]

```
IIonValue ionPerson1 = valueFactory.NewEmptyStruct();
ionPerson1.SetField("FirstName", valueFactory.NewString("Brent"));
ionPerson1.SetField("GovId", valueFactory.NewString("TOYENC486FH"));

IIonValue ionPerson2 = valueFactory.NewEmptyStruct();
ionPerson2.SetField("FirstName", valueFactory.NewString("Jim"));
ionPerson2.SetField("GovId", valueFactory.NewString("ROEE1C1AABH"));

IIonValue ionPeople = valueFactory.NewEmptyList();
ionPeople.Add(ionPerson1);
ionPeople.Add(ionPerson2);

IResult result = driver.Execute(txn =>
{
    return txn.Execute("INSERT INTO Person ?", ionPeople);
});

foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // The statement returns the created documents' ID:
    // {
    //     documentId: "6BFt5eJQDFLBW2aR8LPw42"
    // }
    //
    // {
    //     documentId: "K5Zrcb6N3gmIEHgGhwoyKF"
    // }
}
```

------

传递 Ion 列表时，不要将变量占位符（`?`）括在双尖括号（`<<...>>`）内。在手动 PartiQL 语句中，双尖括号表示名为*bag*的无序集合。

### 更新文档
<a name="cookbook-dotnet.crud.updating"></a>

```
string govId = "TOYENC486FH";
string firstName = "John";

IAsyncResult<Document> result = await driver.Execute(async txn =>
{
    return await txn.Execute(txn.Query<Document>("UPDATE Person SET FirstName = ? WHERE GovId = ?", firstName , govId));
});

await foreach (Document row in result)
{
    Console.WriteLine("{ documentId: " + row.DocumentId + " }");
    // The statement returns the updated document ID:
    // { documentId: Djg30Zoltqy5M4BFsA2jSJ }
}
```

#### 使用 Ion 库
<a name="cookbook-dotnet.crud.updating.ion-library"></a>

------
#### [ Async ]

```
IIonValue ionGovId = valueFactory.NewString("TOYENC486FH");
IIonValue ionFirstName = valueFactory.NewString("John");

IAsyncResult result = await driver.Execute(async txn =>
{
    return await txn.Execute("UPDATE Person SET FirstName = ? WHERE GovId = ?", ionFirstName , ionGovId);
});

await foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // The statement returns the updated document ID:
    // {
    //     documentId: "Djg30Zoltqy5M4BFsA2jSJ"
    // }
}
```

------
#### [ Sync ]

```
IIonValue ionGovId = valueFactory.NewString("TOYENC486FH");
IIonValue ionFirstName = valueFactory.NewString("John");

IResult result = driver.Execute(txn =>
{
    return txn.Execute("UPDATE Person SET FirstName = ? WHERE GovId = ?", ionFirstName , ionGovId);
});

foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // The statement returns the updated document ID:
    // {
    //     documentId: "Djg30Zoltqy5M4BFsA2jSJ"
    // }
}
```

------

**注意**  
在此示例中，我们建议在 `GovId` 字段上设置索引以优化性能。如果不开启`GovId`索引，语句可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

### 删除文档
<a name="cookbook-dotnet.crud.deleting"></a>

```
string govId = "TOYENC486FH";

IAsyncResult<Document> result = await driver.Execute(async txn =>
{
    return await txn.Execute(txn.Query<Document>("DELETE FROM Person WHERE GovId = ?", govId));
});

await foreach (Document row in result)
{
    Console.WriteLine("{ documentId: " + row.DocumentId + " }");
    // The statement returns the updated document ID:
    // { documentId: Djg30Zoltqy5M4BFsA2jSJ }
}
```

#### 使用 Ion 库
<a name="cookbook-dotnet.crud.deleting.ion-library"></a>

------
#### [ Async ]

```
IIonValue ionGovId = valueFactory.NewString("TOYENC486FH");

IAsyncResult result = await driver.Execute(async txn =>
{
    return await txn.Execute("DELETE FROM Person WHERE GovId = ?", ionGovId);
});

await foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // The statement returns the deleted document ID:
    // {
    //     documentId: "Djg30Zoltqy5M4BFsA2jSJ"
    // }
}
```

------
#### [ Sync ]

```
IIonValue ionGovId = valueFactory.NewString("TOYENC486FH");

IResult result = driver.Execute(txn =>
{
    return txn.Execute("DELETE FROM Person WHERE GovId = ?", ionGovId);
});

foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // The statement returns the deleted document ID:
    // {
    //     documentId: "Djg30Zoltqy5M4BFsA2jSJ"
    // }
}
```

------

**注意**  
在此示例中，我们建议在 `GovId` 字段上设置索引以优化性能。如果不开启`GovId`索引，语句可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

### 在一个事务中运行多条语句
<a name="cookbook-dotnet.crud.multi-statement"></a>

```
// 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 async Task<bool> InsureVehicle(IAsyncQldbDriver driver, string vin)
{
    return await driver.Execute(async txn =>
    {
        // Check if the vehicle is insured.
        Amazon.QLDB.Driver.Generic.IAsyncResult<Vehicle> result = await txn.Execute(
            txn.Query<Vehicle>("SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin));

        if (await result.CountAsync() > 0)
        {
            // If the vehicle is not insured, insure it.
            await txn.Execute(
                txn.Query<Document>("UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin));
            return true;
        }
        return false;
    });
}
```

#### 使用 Ion 库
<a name="cookbook-dotnet.crud.multi-statement.ion-library"></a>

------
#### [ Async ]

```
// 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 async Task<bool> InsureVehicle(IAsyncQldbDriver driver, string vin)
{
    ValueFactory valueFactory = new ValueFactory();
    IIonValue ionVin = valueFactory.NewString(vin);

    return await driver.Execute(async txn =>
    {
        // Check if the vehicle is insured.
        Amazon.QLDB.Driver.IAsyncResult result = await txn.Execute(
            "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", ionVin);

        if (await result.CountAsync() > 0)
        {
            // If the vehicle is not insured, insure it.
            await txn.Execute(
                "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", ionVin);
            return true;
        }
        return false;
    });
}
```

------

### 重试逻辑
<a name="cookbook-dotnet.crud.retry-logic"></a>

有关驱动程序的内置重试逻辑的信息，请参见 [使用 Amazon QLDB 中的驱动程序了解重试策略](driver-retry-policy.md)。

### 实现唯一限制
<a name="cookbook-dotnet.crud.uniqueness-constraints"></a>

QLDB 不支持唯一索引，但您可在应用程序中实现此行为。

假设您要对 `Person` 表中的 `GovId` 字段实现唯一性约束。据此，可以编写执行以下操作的事务：

1. 断言该表中没有指定 `GovId` 的现有文档。

1. 如果断言通过，请插入文档。

如果一个竞争事务同时通过断言，则只有一个事务将成功提交。另一笔事务将失败，并显示 OCC 冲突异常。

以下代码示例显示了如何实现此唯一约束。

```
string govId = "TOYENC486FH";

Person person = new Person
{
    GovId = "TOYENC486FH",
    FirstName = "Brent"
};

await driver.Execute(async txn =>
{
    // Check if a document with GovId:TOYENC486FH exists
    // This is critical to make this transaction idempotent
    IAsyncResult<Person> result = await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE GovId = ?", govId));

    // Check if there is a record in the cursor.
    int count = await result.CountAsync();
    if (count > 0)
    {
        // Document already exists, no need to insert
        return;
    }

    // Insert the document.
    await txn.Execute(txn.Query<Document>("INSERT INTO Person ?", person));
});
```

#### 使用 Ion 库
<a name="cookbook-dotnet.crud.uniqueness.ion-library"></a>

------
#### [ Async ]

```
IIonValue ionGovId = valueFactory.NewString("TOYENC486FH");

IIonValue ionPerson = valueFactory.NewEmptyStruct();
ionPerson.SetField("GovId", valueFactory.NewString("TOYENC486FH"));
ionPerson.SetField("FirstName", valueFactory.NewString("Brent"));

await driver.Execute(async txn =>
{
    // Check if a document with GovId:TOYENC486FH exists
    // This is critical to make this transaction idempotent
    IAsyncResult result = await txn.Execute("SELECT * FROM Person WHERE GovId = ?", ionGovId);

    // Check if there is a record in the cursor.
    int count = await result.CountAsync();
    if (count > 0)
    {
        // Document already exists, no need to insert
        return;
    }

    // Insert the document.
    await txn.Execute("INSERT INTO Person ?", ionPerson);
});
```

------
#### [ Sync ]

```
IIonValue ionGovId = valueFactory.NewString("TOYENC486FH");

IIonValue ionPerson = valueFactory.NewEmptyStruct();
ionPerson.SetField("GovId", valueFactory.NewString("TOYENC486FH"));
ionPerson.SetField("FirstName", valueFactory.NewString("Brent"));

driver.Execute(txn =>
{
    // Check if a document with GovId:TOYENC486FH exists
    // This is critical to make this transaction idempotent
    IResult result = txn.Execute("SELECT * FROM Person WHERE GovId = ?", ionGovId);

    // Check if there is a record in the cursor.
    int count = result.Count();
    if (count > 0)
    {
        // Document already exists, no need to insert
        return;
    }

    // Insert the document.
    txn.Execute("INSERT INTO Person ?", ionPerson);
});
```

------

**注意**  
在此示例中，我们建议在 `GovId` 字段上设置索引以优化性能。如果不开启`GovId`索引，语句可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

## 使用 Amazon Ion
<a name="cookbook-dotnet.ion"></a>

可通过多种方法在 QLDB 中处理 Amazon Ion 数据。您可使用 [Ion 库](https://github.com/amzn/ion-dotnet)创建和修改 Ion 值。或者，您可以使用 [Ion 对象映射器](https://github.com/amzn/ion-object-mapper-dotnet)将 C\$1 *普通旧 CLR 对象*（POCO）映射进出 Ion 值。适用 .NET 的 Amazon QLDB 驱动程序 1.3 版本引入了对 Ion 对象映射器的支持。

以下各节提供了使用这两种技术处理 Ion 数据的代码示例。

**Contents**
+ [

### 导入 Ion 模块
](#cookbook-dotnet.ion.import)
+ [

### 创建 Ion 类型
](#cookbook-dotnet.ion.creating-types)
+ [

### 获取 Ion 二进制转储
](#cookbook-dotnet.ion.getting-binary)
+ [

### 获取 Ion 文本转储
](#cookbook-dotnet.ion.getting-text)

### 导入 Ion 模块
<a name="cookbook-dotnet.ion.import"></a>

```
using Amazon.IonObjectMapper;
```

#### 使用 Ion 库
<a name="cookbook-dotnet.ion.import.ion-library"></a>

```
using Amazon.IonDotnet.Builders;
```

### 创建 Ion 类型
<a name="cookbook-dotnet.ion.creating-types"></a>

以下代码示例介绍了如何使用 Ion 对象映射器从 C\$1 对象创建 Ion 值。

```
// Assumes that Person class is defined as follows:
// public class Person
// {
//     public string FirstName { get; set; }
//     public int Age { get; set; }
// }

// Initialize the Ion Object Mapper
IonSerializer ionSerializer = new IonSerializer();

// The C# object to be serialized
Person person = new Person
{
    FirstName = "John",
    Age = 13
};

// Serialize the C# object into stream using the Ion Object Mapper
Stream stream = ionSerializer.Serialize(person);

// Load will take in stream and return a datagram; a top level container of Ion values.
IIonValue ionDatagram = IonLoader.Default.Load(stream);

// To get the Ion value within the datagram, we call GetElementAt(0).
IIonValue ionPerson = ionDatagram.GetElementAt(0);

Console.WriteLine(ionPerson.GetField("firstName").StringValue);
Console.WriteLine(ionPerson.GetField("age").IntValue);
```

#### 使用 Ion 库
<a name="cookbook-dotnet.ion.creating-types.ion-library"></a>

以下代码示例介绍了使用 Ion 库创建 Ion 值的两种方法。

**使用 `ValueFactory`**

```
using Amazon.IonDotnet.Tree;
using Amazon.IonDotnet.Tree.Impl;

IValueFactory valueFactory = new ValueFactory();

IIonValue ionPerson = valueFactory.NewEmptyStruct();
ionPerson.SetField("firstName", valueFactory.NewString("John"));
ionPerson.SetField("age", valueFactory.NewInt(13));

Console.WriteLine(ionPerson.GetField("firstName").StringValue);
Console.WriteLine(ionPerson.GetField("age").IntValue);
```

**使用 `IonLoader`**

```
using Amazon.IonDotnet.Builders;
using Amazon.IonDotnet.Tree;

// Load will take in Ion text and return a datagram; a top level container of Ion values.
IIonValue ionDatagram = IonLoader.Default.Load("{firstName: \"John\", age: 13}");

// To get the Ion value within the datagram, we call GetElementAt(0).
IIonValue ionPerson = ionDatagram.GetElementAt(0);

Console.WriteLine(ionPerson.GetField("firstName").StringValue);
Console.WriteLine(ionPerson.GetField("age").IntValue);
```

### 获取 Ion 二进制转储
<a name="cookbook-dotnet.ion.getting-binary"></a>

```
// Initialize the Ion Object Mapper with Ion binary serialization format
IonSerializer ionSerializer = new IonSerializer(new IonSerializationOptions
{
    Format = IonSerializationFormat.BINARY
});

// The C# object to be serialized
Person person = new Person
{
    FirstName = "John",
    Age = 13
};

MemoryStream stream = (MemoryStream) ionSerializer.Serialize(person);
Console.WriteLine(BitConverter.ToString(stream.ToArray()));
```

#### 使用 Ion 库
<a name="cookbook-dotnet.ion.getting-binary.ion-library"></a>

```
// ionObject is an Ion struct
MemoryStream stream = new MemoryStream();
using (var writer = IonBinaryWriterBuilder.Build(stream))
{
    ionObject.WriteTo(writer);
    writer.Finish();
}

Console.WriteLine(BitConverter.ToString(stream.ToArray()));
```

### 获取 Ion 文本转储
<a name="cookbook-dotnet.ion.getting-text"></a>

```
// Initialize the Ion Object Mapper
IonSerializer ionSerializer = new IonSerializer(new IonSerializationOptions
{
    Format = IonSerializationFormat.TEXT
});

// The C# object to be serialized
Person person = new Person
{
    FirstName = "John",
    Age = 13
};

MemoryStream stream = (MemoryStream) ionSerializer.Serialize(person);
Console.WriteLine(System.Text.Encoding.UTF8.GetString(stream.ToArray()));
```

#### 使用 Ion 库
<a name="cookbook-dotnet.ion.getting-text.ion-library"></a>

```
// ionObject is an Ion struct
StringWriter sw = new StringWriter();
using (var writer = IonTextWriterBuilder.Build(sw))
{
    ionObject.WriteTo(writer);
    writer.Finish();
}

Console.WriteLine(sw.ToString());
```

有关使用 Ion 的更多信息，请参阅上的 [Amazon Ion 文档](http://amzn.github.io/ion-docs/) GitHub。有关在 QLDB 中使用 Ion 的更多代码示例，请参阅[使用 Amazon QLDB 中的 Amazon Ion 数据类型](driver-working-with-ion.md)。

# 适用于 Go 的 Amazon QLDB 驱动程序
<a name="getting-started.golang"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

要处理账本中的数据，您可以使用提供的驱动程序从 Go 应用程序连接到 Amazon QLDB。 AWS 以下主题介绍了如何开始使用适用于 Go 的 QLDB 驱动程序。

**Topics**
+ [

## 驱动程序资源
](#getting-started.golang.resources)
+ [

## 先决条件
](#getting-started.golang.prereqs)
+ [

## 安装
](#getting-started.golang.install)
+ [快速入门教程](driver-quickstart-golang.md)
+ [说明书参考](driver-cookbook-golang.md)

## 驱动程序资源
<a name="getting-started.golang.resources"></a>

有关 Go 驱动程序支持功能的更多信息，请参阅以下资源：
+ API 参考：[3.x](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver)、[2.x](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/v2/qldbdriver)、[1.x](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/qldbdriver)
+ [驱动程序源代码 (GitHub)](https://github.com/awslabs/amazon-qldb-driver-go)
+ [Amazon Ion 说明书](http://amzn.github.io/ion-docs/guides/cookbook.html)

## 先决条件
<a name="getting-started.golang.prereqs"></a>

开始使用适用于 Go 的 QLDB 驱动程序之前，您必须执行以下操作:

1. 按照中的 AWS 设置说明进行操作[访问 Amazon QLDB](accessing.md)。这包括以下这些：

   1. 报名参加 AWS.

   1. 创建具有适当 QLDB 权限的用户。

   1. 授权以编程方式访问开发。

1. （可选）安装您选择的集成式开发环境（IDE）。有关常用于 Go IDEs 的列表，请参阅[编辑器插件和 IDEs](https://golang.org/doc/editors.html) Go 网站。

1. 从 [Go 下载](https://golang.org/dl/)网站下载并安装以下 Go 版本之一：
   + **1.15 或更高版本 **- 适用于 Go v3 的 QLDB 驱动程序
   + **1.14** - 适用于 Go v1 或 v2 的 QLDB 驱动程序

1. 配置您的开发环境用于 [适用于 Go 的 AWS SDK](https://aws.amazon.com/sdk-for-go)：

   1. 设置您的 AWS 凭证。我们建议创建共享的凭证文件。

      有关说明，请参阅*适用于 Go 的 AWS SDK 开发者指南*中的[指定凭证](https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/#specifying-credentials)。

   1. 设置您的默认 AWS 区域。要了解如何操作，请参阅[指定 AWS 区域](https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/#specifying-the-aws-region)。

      有关可用区域的完整列表，请参阅 *AWS 一般参考* 中的 [Amazon QLDB 端点和限额](https://docs.aws.amazon.com/general/latest/gr/qldb.html)。

接下来，您可以设置基本的示例应用程序并运行简短的代码示例，也可以将驱动程序安装到现有的 Go 项目中。
+ 要在现有项目中安装 QLDB 驱动程序和 适用于 Go 的 AWS SDK ，请继续。[安装](#getting-started.golang.install)
+ 要设置项目并运行演示分类账上基本数据事务的简短代码示例，请参阅 [快速入门教程](driver-quickstart-golang.md)。

## 安装
<a name="getting-started.golang.install"></a>

[Go 的 QLDB 驱动程序在存储库 awslabs/ 中 GitHub 是开源的。amazon-qldb-driver-go](http://github.com/awslabs/amazon-qldb-driver-go)QLDB 支持以下驱动程序版本及其 Go 依赖项。


****  

| 驱动程序版本 | Go 版本 | 状态 | 最新发布日期 | 
| --- | --- | --- | --- | 
| [1.x](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/qldbdriver) | 1.14 或更高版本 | 量产版 | 2021 年 6 月 16 日 | 
| [2.x](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/v2/qldbdriver) | 1.14 或更高版本 | 量产版 | 2021 年 7 月 21 日 | 
| [3.x](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver) | 1.15 或更高版本 | 量产版 | 2022 年 11 月 10 日 | 

**安装驱动程序**

1. 确保您的项目使用 [Go 模块](https://blog.golang.org/using-go-modules)来安装项目依赖项。

1. 在您的项目目录中输入以下 `go get` 命令。

------
#### [ 3.x ]

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

------
#### [ 2.x ]

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

------

安装驱动程序还会安装其依赖项，包括 [适用于 Go 的 AWS SDK](https://github.com/aws/aws-sdk-go) 或 [适用于 Go 的 AWS SDK v2 ](https://github.com/aws/aws-sdk-go-v2) 和 [Amazon Ion](https://github.com/amzn/ion-go)软件包。

有关如何在分类账上运行基本数据事务的简短代码示例，请参阅 [说明书参考](driver-cookbook-golang.md)。

# 适用于 Go 的 Amazon QLDB 驱动程序 — 快速入门教程
<a name="driver-quickstart-golang"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在本教程中，您将学习如何使用适用于 Go 的最新版 Amazon QLDB 驱动程序来设置简单应用程序。本指南包括安装驱动程序的步骤以及*创建、读取、更新和删除*（CRUD）的基本操作的简短代码示例。

**Topics**
+ [

## 先决条件
](#driver-quickstart-golang.prereqs)
+ [

## 步骤 1：安装驱动程序
](#driver-quickstart-golang.install)
+ [

## 第 2 步：导入软件包
](#driver-quickstart-golang.import)
+ [

## 第 3 步：初始化驱动程序
](#driver-quickstart-golang.initialize)
+ [

## 第 4 步：创建表和索引
](#driver-quickstart-golang.create-table-index)
+ [

## 第 5 步：插入文档
](#driver-quickstart-golang.insert)
+ [

## 第 6 步：查询文档
](#driver-quickstart-golang.query)
+ [

## 步骤 7：更新文档
](#driver-quickstart-golang.update)
+ [

## 步骤 8：查询更新的文档
](#driver-quickstart-golang.query-2)
+ [

## 第 9 步：删除表格
](#driver-quickstart-golang.drop-table)
+ [

## 运行完整的应用程序
](#driver-quickstart-golang.complete)

## 先决条件
<a name="driver-quickstart-golang.prereqs"></a>

在开始之前，请务必执行以下操作：

1. 请为 Go 驱动程序完成（[先决条件](getting-started.golang.md#getting-started.golang.prereqs)如果尚未执行此操作）。这包括注册 AWS、授予开发所需的编程访问权限以及安装 Go。

1. 创建一个名为 `quick-start` 分类账。

   要了解如何创建分类账，请参阅*控制台入门*中的 [Amazon QLDB 分类账的基本操作](ledger-management.basics.md) 或 [第 1 步：创建新分类账](getting-started-step-1.md)。

## 步骤 1：安装驱动程序
<a name="driver-quickstart-golang.install"></a>

确保您的项目使用 [Go 模块](https://blog.golang.org/using-go-modules)来安装项目依赖项。

在您的项目目录中运行以下 `go get` 命令。

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

安装驱动程序还会安装其依赖项，包括 [适用于 Go 的 AWS SDK v2](https://github.com/aws/aws-sdk-go-v2) 和 [Amazon Ion](https://github.com/amzn/ion-go) 软件包。

## 第 2 步：导入软件包
<a name="driver-quickstart-golang.import"></a>

导入以下 AWS 软件包。

```
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"
)
```

## 第 3 步：初始化驱动程序
<a name="driver-quickstart-golang.initialize"></a>

初始化连接到名为 `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())
```

**注意**  
在此代码示例中，*us-east-1*替换为您创建账本 AWS 区域 的位置。

## 第 4 步：创建表和索引
<a name="driver-quickstart-golang.create-table-index"></a>

以下代码示例显示如何运行 `CREATE TABLE` 和 `CREATE INDEX` 语句。

```
_, 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)
}
```

它会创建一个名为 `People` 的表，并为该表上的 `firstName` 和 `age` 字段创建索引。[索引](ql-reference.create-index.md)是优化查询性能和帮助限制[乐观并发控制（OCC）冲突异常](concurrency.md)所必需的。

## 第 5 步：插入文档
<a name="driver-quickstart-golang.insert"></a>

以下代码示例显示如何运行 `INSERT` 语句。QLDB 支持 [PartiQL](ql-reference.md) 查询语言（兼容 SQL）和 [Amazon Ion](ion.md) 数据格式（JSON 的超集）。

### 使用文字 PartiQL
<a name="driver-quickstart-golang.insert.partiql"></a>

以下代码使用字符串文字 PartiQL 语句将文档插入 `People` 表中。

```
_, 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)
}
```

### 使用 Ion 数据类型
<a name="driver-quickstart-golang.insert.ion"></a>

与 Go 内置的[ JSON 包](https://golang.org/pkg/encoding/json/)类似，您可以在 Ion 中封送和解封送 Go 数据类型。

1. 假设您具有以下名为 `Person` 的 Go 结构。

   ```
   type Person struct {
       FirstName string `ion:"firstName"`
       LastName  string `ion:"lastName"`
       Age       int    `ion:"age"`
   }
   ```

1. 创建 `Person` 的实例。

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

   驱动程序会为您编组 `person` 的一个 ION 编码的文本表示形式。
**重要**  
要使 marshal 和 unmarshal 正常工作，必须导出 Go 数据结构的字段名称（首字母大写）。

1. 将 `person` 实例传递给事务`Execute`的方法。

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

   此示例使用问号（`?`）作为变量占位符，将文档信息传递给语句。使用占位符时，必须传递 ION 编码的文本值。
**提示**  
要使用单个 [INSERT](ql-reference.insert.md) 语句插入多个文档，可以向该语句传递一个[列表](driver-working-with-ion.md#driver-ion-list)类型的参数，如下所示。  

   ```
   // people is a list
   txn.Execute("INSERT INTO People ?", people)
   ```
传递 Ion 列表时，不要将变量占位符（`?`）括在双尖括号（`<<...>>`）内。在手动 PartiQL 语句中，双尖括号表示名为*bag*的无序集合。

## 第 6 步：查询文档
<a name="driver-quickstart-golang.query"></a>

以下代码示例显示如何运行 `SELECT` 语句。

```
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")
}
```

此示例从 `People` 表中查询您的文档，假设结果集不为空，然后从结果中返回您的文档。

## 步骤 7：更新文档
<a name="driver-quickstart-golang.update"></a>

以下代码示例显示如何运行 `UPDATE` 语句。

```
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)
}
```

## 步骤 8：查询更新的文档
<a name="driver-quickstart-golang.query-2"></a>

以下代码示例通过 `firstName` 查询 `People` 表，并返回结果集中的所有文档。

```
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")
}
```

## 第 9 步：删除表格
<a name="driver-quickstart-golang.drop-table"></a>

以下代码示例显示如何运行 `DROP TABLE` 语句。

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

## 运行完整的应用程序
<a name="driver-quickstart-golang.complete"></a>

以下代码示例是 应用程序的完整版本。您还可以从头到尾复制并运行此代码示例，而不必单独执行前面的步骤。此应用程序演示了对名为 `quick-start` 的分类账的一些基本的 CRUD 操作。

**注意**  
在运行此代码之前，请确保 `quick-start` 分类账中还没有名为 `People` 的活动表。

```
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)
    }
}
```

# 适用于 Go 的 Amazon QLDB 驱动程序 — 说明书参考
<a name="driver-cookbook-golang"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

本参考指南显示了 Go 的 Amazon QLDB 驱动程序的常见用例。它提供了 Go 代码示例，演示了如何使用该驱动程序运行基本的*创建、读取、更新和删除*（CRUD）操作。它还包括用于处理 Amazon Ion 数据的代码示例。此外，本指南还重点介绍了使事务具有幂等性和实现唯一性约束的最佳实践。

**注意**  
在适用的情况下，某些用例对于 Go 的 QLDB 驱动程序的每个支持主要版本都配备不同代码示例。

**Contents**
+ [

## 导入驱动程序
](#cookbook-golang.importing)
+ [

## 实例化驱动程序
](#cookbook-golang.instantiating)
+ [

## CRUD 操作
](#cookbook-golang.crud)
  + [

### 创建表
](#cookbook-golang.crud.creating-tables)
  + [

### 创建索引
](#cookbook-golang.crud.creating-indexes)
  + [

### 阅读文档
](#cookbook-golang.crud.reading)
    + [

#### 使用查询参数
](#cookbook-golang.reading-using-params)
  + [

### 插入文档
](#cookbook-golang.crud.inserting)
    + [

#### 在一条语句内插入多个文档
](#cookbook-golang.crud.inserting.multiple)
  + [

### 更新文档
](#cookbook-golang.crud.updating)
  + [

### 删除文档
](#cookbook-golang.crud.deleting)
  + [

### 在一个事务中运行多条语句
](#cookbook-golang.crud.multi-statement)
  + [

### 重试逻辑
](#cookbook-golang.crud.retry-logic)
  + [

### 实现唯一限制
](#cookbook-golang.crud.uniqueness-constraints)
+ [

## 使用 Amazon Ion
](#cookbook-golang.ion)
  + [

### 导入 Ion 模块
](#cookbook-golang.ion.import)
  + [

### 创建 Ion 类型
](#cookbook-golang.ion.creating-types)
  + [

### 获取二进制 Ion
](#cookbook-golang.ion.getting-binary)
  + [

### 获取 Ion 文本
](#cookbook-golang.ion.getting-text)

## 导入驱动程序
<a name="cookbook-golang.importing"></a>

以下代码示例导入驱动程序和其他必需的 AWS 软件包。

------
#### [ 3.x ]

```
import (

    "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"
)
```

------
#### [ 2.x ]

```
import (

    "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"
)
```

------

**注意**  
此示例还导入了 Amazon Ion 软件包（`amzn/ion-go/ion`）。在本参考中运行某些数据操作时，您需要此软件包来处理 Ion 数据。要了解更多信息，请参阅 [使用 Amazon Ion](#cookbook-golang.ion)。

## 实例化驱动程序
<a name="cookbook-golang.instantiating"></a>

以下代码示例在 AWS 区域中创建连接到指定分类账名称的驱动程序实例。

------
#### [ 3.x ]

```
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(
  "vehicle-registration",
  qldbSession,
  func(options *qldbdriver.DriverOptions) {
    options.LoggerVerbosity = qldbdriver.LogInfo
})
if err != nil {
  panic(err)
}

defer driver.Shutdown(context.Background())
```

------
#### [ 2.x ]

```
awsSession := session.Must(session.NewSession(aws.NewConfig().WithRegion("us-east-1")))
qldbSession := qldbsession.New(awsSession)

driver, err := qldbdriver.New(
  "vehicle-registration",
  qldbSession,
  func(options *qldbdriver.DriverOptions) {
    options.LoggerVerbosity = qldbdriver.LogInfo
  })
if err != nil {
  panic(err)
}
```

------

## CRUD 操作
<a name="cookbook-golang.crud"></a>

QLDB 作为事务的一部分运行*创建、读取、更新和删除*（CRUD）操作。

**警告**  
作为最佳实践，使写事务严格地幂等。

**使事务幂等**

我们建议将写事务设置为幂等，以避免重试时出现任何意想不到的副作用。如果事务可以运行多次并每次都产生相同的结果，则事务是*幂等的*。

例如，假设有一个事务，要将文档插入名为 `Person` 的表中。事务应该首先检查文档是否已经存在于表中。如果没有这种检查，表最终可能会有重复的文档。

假设 QLDB 在服务器端成功提交了事务，但客户端在等待响应时超时。如果事务不是幂等的，则在重试的情况下可以多次插入相同的文档。

**使用索引避免全表扫描**

我们还建议您在索引字段或文档 ID 上使用*相等*运算符来运行带有`WHERE`谓词子句的语句；例如，`WHERE indexedField = 123`或 `WHERE indexedField IN (456, 789)`。如果没有这种索引查找，QLDB 需要进行表扫描，这可能会导致事务超时或*乐观并发控制*（OCC）冲突。

有关 OCC 的更多信息，请参阅[Amazon QLDB 并发模型](concurrency.md)。

**隐式创建的事务**

[QLDBDriver.Execut](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver#QLDBDriver.Execute) e 函数接受一个 lambda 函数，该函数接收[交易](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver#Transaction)实例，您可以使用该实例来运行语句。的实例`Transaction`封装了隐式创建的事务。

您可以使用`Transaction.Execute`函数在 lambda 函数中运行语句。当 lambda 函数返回时，驱动程序会隐式提交事务。

以下各节介绍如何运行基本的 CRUD 操作、指定自定义重试逻辑以及如何实现唯一性约束。

**Contents**
+ [

### 创建表
](#cookbook-golang.crud.creating-tables)
+ [

### 创建索引
](#cookbook-golang.crud.creating-indexes)
+ [

### 阅读文档
](#cookbook-golang.crud.reading)
  + [

#### 使用查询参数
](#cookbook-golang.reading-using-params)
+ [

### 插入文档
](#cookbook-golang.crud.inserting)
  + [

#### 在一条语句内插入多个文档
](#cookbook-golang.crud.inserting.multiple)
+ [

### 更新文档
](#cookbook-golang.crud.updating)
+ [

### 删除文档
](#cookbook-golang.crud.deleting)
+ [

### 在一个事务中运行多条语句
](#cookbook-golang.crud.multi-statement)
+ [

### 重试逻辑
](#cookbook-golang.crud.retry-logic)
+ [

### 实现唯一限制
](#cookbook-golang.crud.uniqueness-constraints)

### 创建表
<a name="cookbook-golang.crud.creating-tables"></a>

```
result, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
  return txn.Execute("CREATE TABLE Person")
})
```

### 创建索引
<a name="cookbook-golang.crud.creating-indexes"></a>

```
result, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
  return txn.Execute("CREATE INDEX ON Person(GovId)")
})
```

### 阅读文档
<a name="cookbook-golang.crud.reading"></a>

```
var decodedResult map[string]interface{}

// Assumes that Person table has documents as follows:
// { "GovId": "TOYENC486FH", "FirstName": "Brent" }
_, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
  result, err := txn.Execute("SELECT * FROM Person WHERE GovId = 'TOYENC486FH'")
  if err != nil {
    return nil, err
  }
  for result.Next(txn) {
    ionBinary := result.GetCurrentData()
    err = ion.Unmarshal(ionBinary, &decodedResult)
    if err != nil {
      return nil, err
    }
    fmt.Println(decodedResult) // prints map[GovId: TOYENC486FH FirstName:Brent]
  }
  if result.Err() != nil {
    return nil, result.Err()
  }
  return nil, nil
})
if err != nil {
  panic(err)
}
```

#### 使用查询参数
<a name="cookbook-golang.reading-using-params"></a>

以下代码示例使用原生类型查询参数。

```
result, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
  return txn.Execute("SELECT * FROM Person WHERE GovId = ?", "TOYENC486FH")
})
if err != nil {
  panic(err)
}
```

以下代码示例使用了多个查询参数。

```
result, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
  return txn.Execute("SELECT * FROM Person WHERE GovId = ? AND FirstName = ?", "TOYENC486FH", "Brent")
})
if err != nil {
  panic(err)
}
```

以下代码示例使用查询参数列表。

```
govIDs := []string{}{"TOYENC486FH", "ROEE1", "YH844"}

result, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
  return txn.Execute("SELECT * FROM Person WHERE GovId IN (?,?,?)", govIDs...)
})
if err != nil {
  panic(err)
}
```

**注意**  
当您在没有索引查找的情况下运行查询时，它会调用全表扫描。在此示例中，我们建议在`GovId`字段上设置 [索引](ql-reference.create-index.md)以优化性能。如果不开启`GovId`索引，查询可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

### 插入文档
<a name="cookbook-golang.crud.inserting"></a>

以下代码示例插入本地数据类型。

```
_, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
  // Check if a document with a GovId of TOYENC486FH exists
  // This is critical to make this transaction idempotent
  result, err := txn.Execute("SELECT * FROM Person WHERE GovId = ?", "TOYENC486FH")
  if err != nil {
    return nil, err
  }
  // Check if there are any results
  if result.Next(txn) {
    // Document already exists, no need to insert
  } else {
    person := map[string]interface{}{
      "GovId": "TOYENC486FH",
      "FirstName": "Brent",
    }
    _, err = txn.Execute("INSERT INTO Person ?", person)
    if err != nil {
      return nil, err
    }
  }
  return nil, nil
})
```

此事务将文档插入 `Person` 表中。在插入之前，它首先检查文档是否已存在于表格内。**此检查使事务本质上是幂等。**即使您多次运行此事务，也不会造成任何异常副作用。

**注意**  
在此示例中，我们建议在 `GovId` 字段上设置索引以优化性能。如果不开启`GovId`索引，语句可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

#### 在一条语句内插入多个文档
<a name="cookbook-golang.crud.inserting.multiple"></a>

要使用单个 [INSERT](ql-reference.insert.md) 语句插入多个文档，可以向该语句传递一个[列表](driver-working-with-ion.md#driver-ion-list)类型的参数，如下所示。

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

传递 Ion 列表时，不要将变量占位符（`?`）括在双尖括号（`<<...>>`）内。在手动 PartiQL 语句中，双尖括号表示名为*bag*的无序集合。

### 更新文档
<a name="cookbook-golang.crud.updating"></a>

以下代码示例使用原生数据类型。

```
result, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
  return txn.Execute("UPDATE Person SET FirstName = ? WHERE GovId = ?", "John", "TOYENC486FH")
})
```

**注意**  
在此示例中，我们建议在 `GovId` 字段上设置索引以优化性能。如果不开启`GovId`索引，语句可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

### 删除文档
<a name="cookbook-golang.crud.deleting"></a>

以下代码示例使用原生数据类型。

```
result, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
  return txn.Execute("DELETE FROM Person WHERE GovId = ?", "TOYENC486FH")
})
```

**注意**  
在此示例中，我们建议在 `GovId` 字段上设置索引以优化性能。如果不开启`GovId`索引，语句可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

### 在一个事务中运行多条语句
<a name="cookbook-golang.crud.multi-statement"></a>

```
// 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.
func InsureCar(driver *qldbdriver.QLDBDriver, vin string) (bool, error) {
    insured, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {

        result, err := txn.Execute(
            "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin)
        if err != nil {
            return false, err
        }

        hasNext := result.Next(txn)
        if !hasNext && result.Err() != nil {
            return false, result.Err()
        }

        if hasNext {
            _, err = txn.Execute(
                "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin)
            if err != nil {
                return false, err
            }
            return true, nil
        }
        return false, nil
    })
    if err != nil {
        panic(err)
    }

    return insured.(bool), err
}
```

### 重试逻辑
<a name="cookbook-golang.crud.retry-logic"></a>

驱动程序 `Execute` 函数具有内置的重试机制，如果发生可重试的异常（例如超时或 OCC 冲突），该机制可以重试事务。最大重试次数和退避策略为可配置。

默认的重试限制为`4`，默认的退避策略以毫秒[ExponentialBackoffStrategy](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver#ExponentialBackoffStrategy)为基准。`10`您可以使用的实例为每个驱动程序实例以及每个事务设置重试策略。[RetryPolicy](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver#RetryPolicy)

以下代码示例使用自定义重试限制和驱动程序实例的自定义退避策略指定重试逻辑。

```
import (
  "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)

  // Configuring retry limit to 2
  retryPolicy := qldbdriver.RetryPolicy{MaxRetryLimit: 2}

  driver, err := qldbdriver.New("test-ledger", qldbSession, func(options *qldbdriver.DriverOptions) {
    options.RetryPolicy = retryPolicy
  })
  if err != nil {
    panic(err)
  }

  // Configuring an exponential backoff strategy with base of 20 milliseconds
  retryPolicy = qldbdriver.RetryPolicy{
    MaxRetryLimit: 2,
    Backoff: qldbdriver.ExponentialBackoffStrategy{SleepBase: 20, SleepCap: 4000,
    }}

  driver, err = qldbdriver.New("test-ledger", qldbSession, func(options *qldbdriver.DriverOptions) {
    options.RetryPolicy = retryPolicy
  })
  if err != nil {
    panic(err)
  }
}
```

以下代码示例使用自定义重试限制和自定义回退策略指定重试逻辑，用于特定匿名函数。该 `SetRetryPolicy` 函数将覆盖为驱动程序实例设置的重试策略。

```
import (
  "context"
  "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)

  // Configuring retry limit to 2
  retryPolicy1 := qldbdriver.RetryPolicy{MaxRetryLimit: 2}

  driver, err := qldbdriver.New("test-ledger", qldbSession, func(options *qldbdriver.DriverOptions) {
    options.RetryPolicy = retryPolicy1
  })
  if err != nil {
    panic(err)
  }

  // Configuring an exponential backoff strategy with base of 20 milliseconds
  retryPolicy2 := qldbdriver.RetryPolicy{
    MaxRetryLimit: 2,
    Backoff: qldbdriver.ExponentialBackoffStrategy{SleepBase: 20, SleepCap: 4000,
    }}

  // Overrides the retry policy set by the driver instance
  driver.SetRetryPolicy(retryPolicy2)

  driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
    return txn.Execute("CREATE TABLE Person")
  })
}
```

### 实现唯一限制
<a name="cookbook-golang.crud.uniqueness-constraints"></a>

QLDB 不支持唯一索引，但您可在应用程序中实现此行为。

假设您要对 `Person` 表中的 `GovId` 字段实现唯一性约束。据此，可以编写执行以下操作的事务：

1. 断言该表中没有指定 `GovId` 的现有文档。

1. 如果断言通过，请插入文档。

如果一个竞争事务同时通过断言，则只有一个事务将成功提交。另一笔事务将失败，并显示 OCC 冲突异常。

以下代码示例显示了如何实现此唯一约束。

```
govID := "TOYENC486FH"

document := map[string]interface{}{
  "GovId":     "TOYENC486FH",
  "FirstName": "Brent",
}

result, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
  // Check if doc with GovId = govID exists
  result, err := txn.Execute("SELECT * FROM Person WHERE GovId = ?", govID)
  if err != nil {
    return nil, err
  }
  // Check if there are any results
  if result.Next(txn) {
    // Document already exists, no need to insert
    return nil, nil
  }
  return txn.Execute("INSERT INTO Person ?", document)
})
if err != nil {
  panic(err)
}
```

**注意**  
在此示例中，我们建议在 `GovId` 字段上设置索引以优化性能。如果不开启`GovId`索引，语句可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

## 使用 Amazon Ion
<a name="cookbook-golang.ion"></a>

以下各节说明了如何使用 Amazon Ion 模块处理 Ion 数据。

**Contents**
+ [

### 导入 Ion 模块
](#cookbook-golang.ion.import)
+ [

### 创建 Ion 类型
](#cookbook-golang.ion.creating-types)
+ [

### 获取二进制 Ion
](#cookbook-golang.ion.getting-binary)
+ [

### 获取 Ion 文本
](#cookbook-golang.ion.getting-text)

### 导入 Ion 模块
<a name="cookbook-golang.ion.import"></a>

```
import "github.com/amzn/ion-go/ion"
```

### 创建 Ion 类型
<a name="cookbook-golang.ion.creating-types"></a>

Go 版 Ion 库当前不支持文档对象模型（DOM），因此您无法创建 Ion 数据类型。但是使用 QLDB 时，您可以在 Go 原生类型和 Ion 二进制文件之间进行编组和解组。

### 获取二进制 Ion
<a name="cookbook-golang.ion.getting-binary"></a>

```
aDict := map[string]interface{}{
  "GovId": "TOYENC486FH",
  "FirstName": "Brent",
}

ionBytes, err := ion.MarshalBinary(aDict)
if err != nil {
  panic(err)
}

fmt.Println(ionBytes) // prints [224 1 0 234 238 151 129 131 222 147 135 190 144 133 71 111 118 73 100 137 70 105 114 115 116 78 97 109 101 222 148 138 139 84 79 89 69 78 67 52 56 54 70 72 139 133 66 114 101 110 116]
```

### 获取 Ion 文本
<a name="cookbook-golang.ion.getting-text"></a>

```
aDict := map[string]interface{}{
  "GovId": "TOYENC486FH",
  "FirstName": "Brent",
}

ionBytes, err := ion.MarshalText(aDict)
if err != nil {
  panic(err)
}

fmt.Println(string(ionBytes)) // prints {FirstName:"Brent",GovId:"TOYENC486FH"}
```

有关 Ion 的更多信息，请参阅上的 [Amazon Ion 文档](http://amzn.github.io/ion-docs/) GitHub。有关在 QLDB 中使用 Ion 的更多代码示例，请参阅[使用 Amazon QLDB 中的 Amazon Ion 数据类型](driver-working-with-ion.md)。

# 适用于 Node.js 的 Amazon QLDB 驱动程序
<a name="getting-started.nodejs"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

要处理账本中的数据，您可以使用提供的驱动程序从 Node.js 应用程序连接到 Amazon QLDB。 AWS 以下主题介绍了如何开始使用 Node.js 上 QLDB 驱动程序。

**Topics**
+ [

## 驱动程序资源
](#getting-started.nodejs.resources)
+ [

## 先决条件
](#getting-started.nodejs.prereqs)
+ [

## 安装
](#getting-started.nodejs.install)
+ [

## 设置建议
](#nodejs-setup)
+ [快速入门教程](driver-quickstart-nodejs.md)
+ [说明书参考](driver-cookbook-nodejs.md)

## 驱动程序资源
<a name="getting-started.nodejs.resources"></a>

有关 Node.js 驱动程序支持功能的更多信息，请参阅以下资源：
+ API 参考：[3.x](https://amazon-qldb-docs.s3.amazonaws.com/drivers/nodejs/3.1.0/index.html)、[2.x](https://amazon-qldb-docs.s3.amazonaws.com/drivers/nodejs/2.2.0/index.html)、[1.x](https://amazon-qldb-docs.s3.amazonaws.com/drivers/nodejs/1.0.0/index.html)
+ [驱动程序源代码 (GitHub)](https://github.com/awslabs/amazon-qldb-driver-nodejs)
+ [示例应用程序源代码 (GitHub)](https://github.com/aws-samples/amazon-qldb-dmv-sample-nodejs)
+ [Amazon Ion 代码示例](ion.code-examples.md)
+ [使用（博客）在 QL AWS Lambda DB 上构建简单的 CRUD 操作和数据流AWS](https://aws.amazon.com/blogs/database/build-a-simple-crud-operation-and-data-stream-on-amazon-qldb-using-aws-lambda/)

## 先决条件
<a name="getting-started.nodejs.prereqs"></a>

开始使用适用于 Node.js 的 QLDB 驱动程序之前，您必须执行以下操作:

1. 按照中的 AWS 设置说明进行操作[访问 Amazon QLDB](accessing.md)。这包括以下这些：

   1. 报名参加 AWS.

   1. 创建具有适当 QLDB 权限的用户。

   1. 授权以编程方式访问开发。

1. 从 [Node.js 下载](https://nodejs.org/en/download/) 网站安装 Node.js 14.x 或更高版本。（该驱动程序先前版本支持 Node.js 版本 10.x 或更高版本。）

1. 在 [Node.js JavaScript 中为AWS SDK](https://aws.amazon.com/sdk-for-node-js) 配置开发环境：

   1. 设置您的 AWS 凭证。我们建议创建共享的凭证文件。

      有关说明，请参阅*适用于 JavaScript 的 AWS SDK 开发指南*中的[从共享凭据文件加载 Node.js 中的凭据](https://docs.aws.amazon.com/sdk-for-javascript/latest/developer-guide/loading-node-credentials-shared.html)。

   1. 设置您的默认 AWS 区域。要了解如何使用，请参阅 [设置 AWS 区域](https://docs.aws.amazon.com/sdk-for-javascript/latest/developer-guide/setting-region.html)。

      有关可用区域的完整列表，请参阅 *AWS 一般参考* 中的 [Amazon QLDB 端点和限额](https://docs.aws.amazon.com/general/latest/gr/qldb.html)。

接下来，您可下载完整的教程示例应用程序，也可以只在 Node.js 项目中安装驱动程序并运行短代码示例。
+ 要在现有项目中安装 Node.js 中的 QLDB 驱动程序和 AWS JavaScript SDK，请继续。[安装](#getting-started.nodejs.install)
+ 要设置项目并运行演示分类账上基本数据事务的简短代码示例，请参阅 [快速入门教程](driver-quickstart-nodejs.md)。
+ 要在完整的教程示例应用程序中运行更深入的数据和管理 API 操作示例，请参阅 [Node.js 教程](getting-started.nodejs.tutorial.md)。

## 安装
<a name="getting-started.nodejs.install"></a>

QLDB 支持以下驱动程序版本及其 Node.js 依赖项。


****  

| 驱动程序版本 | Node.js 版本 | 状态 | 最新发布日期 | 
| --- | --- | --- | --- | 
| [1.x](https://www.npmjs.com/package/amazon-qldb-driver-nodejs/v/1.0.0) | 10.x 或更高版本 | 量产版 | 2020 年 6 月 5 日 | 
|  [2.x](https://www.npmjs.com/package/amazon-qldb-driver-nodejs/v/2.2.0) | 10.x 或更高版本 | 量产版 | 2021 年 5 月 6 日 | 
| [3.x](https://www.npmjs.com/package/amazon-qldb-driver-nodejs/v/3.1.0) | 14.x 或更高版本 | 量产版 | 2023 年 11 月 10 日 | 

要[使用 npm（Node.js 包管理器）](https://www.npmjs.com/)安装 QLDB 驱动程序，请从项目根目录中输入以下命令。

------
#### [ 3.x ]

```
npm install amazon-qldb-driver-nodejs
```

------
#### [ 2.x ]

```
npm install amazon-qldb-driver-nodejs@2.2.0
```

------
#### [ 1.x ]

```
npm install amazon-qldb-driver-nodejs@1.0.0
```

------

驱动程序对以下软件包具有对等依赖项。您还必须将这些软件包作为依赖项安装至项目中。

------
#### [ 3.x ]

模块化聚合 QLDB 客户端（管理 API）

```
npm install @aws-sdk/client-qldb
```

模块化聚合 *QLDB* 会话客户端（数据 API）

```
npm install @aws-sdk/client-qldb-session
```

Amazon Ion 数据格式

```
npm install ion-js
```

纯粹的 JavaScript 实现 `BigInt`

```
npm install jsbi
```

------
#### [ 2.x ]

适用于 JavaScript 的 AWS SDK

```
npm install aws-sdk
```

Amazon Ion 数据格式

```
npm install ion-js@4.0.0
```

纯粹的 JavaScript 实现 `BigInt`

```
npm install jsbi@3.1.1
```

------
#### [ 1.x ]

适用于 JavaScript 的 AWS SDK

```
npm install aws-sdk
```

Amazon Ion 数据格式

```
npm install ion-js@4.0.0
```

纯粹的 JavaScript 实现 `BigInt`

```
npm install jsbi@3.1.1
```

------

**使用驱动程序连接至分类账**

然后，您可以导入驱动程序，并使用它来连接到分类账。以下 TypeScript 代码示例说明如何为指定的账本名称和创建驱动程序实例 AWS 区域。

------
#### [ 3.x ]

```
import { Agent } from 'https';
import { QLDBSessionClientConfig } from "@aws-sdk/client-qldb-session";
import { QldbDriver, RetryConfig  } from 'amazon-qldb-driver-nodejs';
import { NodeHttpHandlerOptions } from "@aws-sdk/node-http-handler";

const maxConcurrentTransactions: number = 10;
const retryLimit: number = 4;

//Reuse connections with keepAlive
const lowLevelClientHttpOptions: NodeHttpHandlerOptions = {
    httpAgent: new Agent({
      maxSockets: maxConcurrentTransactions
    })
};

const serviceConfigurationOptions: QLDBSessionClientConfig = {
    region: "us-east-1"
};

//Use driver's default backoff function for this example (no second parameter provided to RetryConfig)
const retryConfig: RetryConfig = new RetryConfig(retryLimit);
const qldbDriver: QldbDriver = new QldbDriver("testLedger", serviceConfigurationOptions, lowLevelClientHttpOptions, maxConcurrentTransactions, retryConfig);

qldbDriver.getTableNames().then(function(tableNames: string[]) {
    console.log(tableNames);
});
```

------
#### [ 2.x ]

```
import { Agent } from 'https';
import { QldbDriver, RetryConfig  } from 'amazon-qldb-driver-nodejs';

const maxConcurrentTransactions: number = 10;
const retryLimit: number = 4;

//Reuse connections with keepAlive
const agentForQldb: Agent = new Agent({
    keepAlive: true,
    maxSockets: maxConcurrentTransactions
});

const serviceConfigurationOptions = {
    region: "us-east-1",
    httpOptions: {
        agent: agentForQldb
    }
};

//Use driver's default backoff function for this example (no second parameter provided to RetryConfig)
const retryConfig: RetryConfig = new RetryConfig(retryLimit);
const qldbDriver: QldbDriver = new QldbDriver("testLedger", serviceConfigurationOptions, maxConcurrentTransactions, retryConfig);

qldbDriver.getTableNames().then(function(tableNames: string[]) {
    console.log(tableNames);
});
```

------
#### [ 1.x ]

```
import { Agent } from 'https';
import { QldbDriver } from 'amazon-qldb-driver-nodejs';

const poolLimit: number = 10;
const retryLimit: number = 4;

//Reuse connections with keepAlive
const agentForQldb: Agent = new Agent({
    keepAlive: true,
    maxSockets: poolLimit
});

const serviceConfigurationOptions = {
    region: "us-east-1",
    httpOptions: {
        agent: agentForQldb
    }
};

const qldbDriver: QldbDriver = new QldbDriver("testLedger", serviceConfigurationOptions, retryLimit, poolLimit);
qldbDriver.getTableNames().then(function(tableNames: string[]) {
    console.log(tableNames);
});
```

------

有关如何在分类账上运行基本数据交易的简短代码示例，请参阅 [说明书参考](driver-cookbook-nodejs.md)。

## 设置建议
<a name="nodejs-setup"></a>

### 重复使用具有保持连接功能的连接
<a name="nodejs-setup.keepalive"></a>

#### QLDB Node.js 驱动程序 v3
<a name="nodejs-setup.keepalive-v3"></a>

默认的 Node.js HTTP/HTTPS 代理会为每个新请求创建一个新的 TCP 连接。为了避免重建连接的成本， 适用于 JavaScript 的 AWS SDK v3 默认会重复使用 TCP 连接。*有关更多信息以及如何禁用 “重用” 连接，请参阅适用于 JavaScript 的 AWS SDK 开发人员指南*中的[在 Node.js 中通过 keep-alive 重用连接](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/node-reusing-connections.html)。

我们建议使用默认设置来重用 Node.js 的 QLDB 驱动程序中的连接。在驱动程序初始化期间，将低级客户端 HTTP 选项 `maxSockets` 设置为`maxConcurrentTransactions` 与您设置的值相同。

例如，请参阅以下内容 JavaScript 或 TypeScript 代码。

------
#### [ JavaScript ]

```
const qldb = require('amazon-qldb-driver-nodejs');
const https = require('https');

//Replace this value as appropriate for your application
const maxConcurrentTransactions = 50;

const agentForQldb = new https.Agent({
    //Set this to the same value as `maxConcurrentTransactions`(previously called `poolLimit`)
    //Do not rely on the default value of `Infinity`
    "maxSockets": maxConcurrentTransactions
});

const lowLevelClientHttpOptions = {
    httpAgent: agentForQldb
}

let driver = new qldb.QldbDriver("testLedger", undefined, lowLevelClientHttpOptions, maxConcurrentTransactions);
```

------
#### [ TypeScript ]

```
import { Agent } from 'https';
import { NodeHttpHandlerOptions } from "@aws-sdk/node-http-handler";
import { QldbDriver } from 'amazon-qldb-driver-nodejs';

//Replace this value as appropriate for your application
const maxConcurrentTransactions: number = 50;

const agentForQldb: Agent = new Agent({
    //Set this to the same value as `maxConcurrentTransactions`(previously called `poolLimit`)
    //Do not rely on the default value of `Infinity`
    maxSockets: maxConcurrentTransactions
});

const lowLevelClientHttpOptions: NodeHttpHandlerOptions = {
    httpAgent: agentForQldb
};

let driver = new QldbDriver("testLedger", undefined, lowLevelClientHttpOptions, maxConcurrentTransactions);
```

------

#### QLDB Node.js 驱动程序 v2
<a name="nodejs-setup.keepalive-v2"></a>

默认的 Node.js HTTP/HTTPS 代理会为每个新请求创建一个新的 TCP 连接。为了节省建立新连接的成本，建议重复使用现有的连接。

要重用 Node.js 的 QLDB 驱动程序中的连接，请使用以下选项之一：
+ 在驱动程序初始化时，设置以下低级客户端 HTTP 选项：
  + `keepAlive` – `true`
  + `maxSockets` — 与您设置的`maxConcurrentTransactions`值相同

  例如，请参阅以下内容 JavaScript 或 TypeScript 代码。

------
#### [ JavaScript ]

  ```
  const qldb = require('amazon-qldb-driver-nodejs');
  const https = require('https');
  
  //Replace this value as appropriate for your application
  const maxConcurrentTransactions = 50;
  
  const agentForQldb = new https.Agent({
      "keepAlive": true,
      //Set this to the same value as `maxConcurrentTransactions`(previously called `poolLimit`)
      //Do not rely on the default value of `Infinity`
      "maxSockets": maxConcurrentTransactions
  });
  
  const serviceConfiguration = { "httpOptions": {
      "agent": agentForQldb
  }};
  
  let driver = new qldb.QldbDriver("testLedger", serviceConfiguration, maxConcurrentTransactions);
  ```

------
#### [ TypeScript ]

  ```
  import { Agent } from 'https';
  import { ClientConfiguration } from 'aws-sdk/clients/acm';
  import { QldbDriver } from 'amazon-qldb-driver-nodejs';
  
  //Replace this value as appropriate for your application
  const maxConcurrentTransactions: number = 50;
  
  const agentForQldb: Agent = new Agent({
      keepAlive: true,
      //Set this to the same value as `maxConcurrentTransactions`(previously called `poolLimit`)
      //Do not rely on the default value of `Infinity`
      maxSockets: maxConcurrentTransactions
  });
  
  const serviceConfiguration: ClientConfiguration = { httpOptions: {
      agent: agentForQldb
  }};
  
  let driver = new QldbDriver("testLedger", serviceConfiguration, maxConcurrentTransactions);
  ```

------
+ 或者，您也可以设置 `AWS_NODEJS_CONNECTION_REUSE_ENABLED` 环境变量为 `1`。有关示例，请参阅 *适用于 JavaScript 的 AWS SDK 开发人员指南* 中的[在 Node.js 中重复使用具有保持连接功能的连接](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/node-reusing-connections.html)。
**注意**  
如果您设置此环境变量，它将影响所有使用 适用于 JavaScript 的 AWS SDK的 AWS 服务 。

# 适用于 Node.js 的 Amazon QLDB 驱动程序 — 快速入门教程
<a name="driver-quickstart-nodejs"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在本教程中，您将学习如何使用适用于 Node.js 的 Amazon QLDB 驱动程序来设置简单应用程序。本指南包括安装驱动程序的步骤，以及基本*创建、读取、更新 JavaScript 和删除 (CRUD) 操作的简短和 TypeScript *代码示例。有关在完整示例应用程序中演示这些操作的更深入的示例，请参阅 [Node.js 教程](getting-started.nodejs.tutorial.md)。

**注意**  
在适用的情况下，某些步骤对于 Node.js 的 QLDB 驱动程序的每个支持主要版本都配备不同代码示例。

**Topics**
+ [

## 先决条件
](#driver-quickstart-nodejs.prereqs)
+ [

## 第 1 步：设置您的 项目
](#driver-quickstart-nodejs.step-1)
+ [

## 第 2 步：初始化驱动程序
](#driver-quickstart-nodejs.step-2)
+ [

## 第 3 步：创建表和索引
](#driver-quickstart-nodejs.step-3)
+ [

## 第 4 步：插入文档
](#driver-quickstart-nodejs.step-4)
+ [

## 第 5 步：查询文档
](#driver-quickstart-nodejs.step-5)
+ [

## 第 6 步：更新文档
](#driver-quickstart-nodejs.step-6)
+ [

## 运行完整的应用程序
](#driver-quickstart-nodejs.complete)

## 先决条件
<a name="driver-quickstart-nodejs.prereqs"></a>

在开始之前，请务必执行以下操作：

1. 请为 Node.js 驱动程序完成（[先决条件](getting-started.nodejs.md#getting-started.nodejs.prereqs)如果尚未执行此操作）。这包括注册 AWS、授予开发所需的编程访问权限以及安装 Node.js。

1. 创建一个名为 `quick-start` 分类账。

   要了解如何创建分类账，请参阅*控制台入门*中的 [Amazon QLDB 分类账的基本操作](ledger-management.basics.md) 或 [第 1 步：创建新分类账](getting-started-step-1.md)。

如果您正在使用 TypeScript，则还必须执行以下设置步骤。

### 使用 TypeScript
<a name="driver-quickstart-nodejs.prereqs.ts"></a>

**要安装 TypeScript**

1. 安装 TypeScript 软件包。QLDB 驱动程序在 3.8.x 上运行。 TypeScript 

   ```
   $ npm install --global typescript@3.8.0
   ```

1. 安装软件包后，运行以下命令以确保 TypeScript 编译器已安装。

   ```
   $ tsc --version
   ```

要按以下步骤运行代码，请注意必须先将 TypeScript 文件转换为可执行 JavaScript 代码，如下所示。

```
$ tsc app.ts; node app.js
```

## 第 1 步：设置您的 项目
<a name="driver-quickstart-nodejs.step-1"></a>

首先，您需要设置您的 Node.js 项目。

1. 为应用程序创建一个文件夹。

   ```
   $ mkdir myproject
   $ cd myproject
   ```

1. 要初始化您的项目，请输入以下 `npm` 命令并回答安装过程中提出的问题。您可以对大多数问题使用默认值。

   ```
   $ npm init
   ```

1. 安装适用于 Node.js 的Amazon QLDB 驱动程序。
   + 使用版本 3.x

     ```
     $ npm install amazon-qldb-driver-nodejs --save
     ```
   + 使用版本 2.x

     ```
     $ npm install amazon-qldb-driver-nodejs@2.2.0 --save
     ```
   + 使用版本 1.x

     ```
     $ npm install amazon-qldb-driver-nodejs@1.0.0 --save
     ```

1. 安装驱动程序的对等依赖项。
   + 使用版本 3.x

     ```
     $ npm install @aws-sdk/client-qldb-session --save
     $ npm install ion-js --save
     $ npm install jsbi --save
     ```
   + 使用版本 2.x 或 1.x

     ```
     $ npm install aws-sdk --save
     $ npm install ion-js@4.0.0 --save
     $ npm install jsbi@3.1.1 --save
     ```

1. 创建一个名为 JavaScript、或`app.js``app.ts`为的新文件 TypeScript。

   然后，按以下步骤中逐步添加代码示例，尝试一些基本的 CRUD 操作。或者，您可以跳过本 step-by-step教程，改为运行[完整的应用程序](#driver-quickstart-nodejs.complete)。

## 第 2 步：初始化驱动程序
<a name="driver-quickstart-nodejs.step-2"></a>

初始化连接到名为 `quick-start` 的分类账的驱动程序实例。将以下代码添加到您的 `app.js` 或 `app.ts` 文件。

### 使用版本 3.x
<a name="driver-quickstart-nodejs.step-2-v3"></a>

------
#### [ JavaScript ]

```
var qldb = require('amazon-qldb-driver-nodejs');
var https = require('https');

function main() {
    const maxConcurrentTransactions = 10;
    const retryLimit = 4;

    const agentForQldb = new https.Agent({
        maxSockets: maxConcurrentTransactions
    });

    const lowLevelClientHttpOptions = {
       httpAgent: agentForQldb
    }

    const serviceConfigurationOptions = {
        region: "us-east-1"
    };

    // Use driver's default backoff function for this example (no second parameter provided to RetryConfig)
    var retryConfig = new qldb.RetryConfig(retryLimit);
    var driver = new qldb.QldbDriver("quick-start", serviceConfigurationOptions, lowlevelClientHttpOptions, maxConcurrentTransactions, retryConfig);
}

main();
```

------
#### [ TypeScript ]

```
import { Agent } from "https";
import { NodeHttpHandlerOptions } from "@aws-sdk/node-http-handler";
import { QLDBSessionClientConfig } from "@aws-sdk/client-qldb-session";
import { QldbDriver, RetryConfig } from "amazon-qldb-driver-nodejs";

function main(): void {
    const maxConcurrentTransactions: number = 10;
    const agentForQldb: Agent = new Agent({
        maxSockets: maxConcurrentTransactions
    });

    const lowLevelClientHttpOptions: NodeHttpHandlerOptions = {
      httpAgent: agentForQldb
    };

    const serviceConfigurationOptions: QLDBSessionClientConfig = {
        region: "us-east-1"
    };

    const retryLimit: number = 4;
    // Use driver's default backoff function for this example (no second parameter provided to RetryConfig)
    const retryConfig: RetryConfig = new RetryConfig(retryLimit);
    const driver: QldbDriver = new QldbDriver("quick-start", serviceConfigurationOptions, lowLevelClientHttpOptions, maxConcurrentTransactions, retryConfig);
}

if (require.main === module) {
    main();
}
```

------

**注意**  
在此代码示例中，*us-east-1*替换为您创建账本 AWS 区域 的位置。
为简单起见，本指南中的其余代码示例使用具有默认设置的驱动程序，如以下版本 1.x 示例中所述。您也可以使用您自己的驱动程序实例来代替自定义 `RetryConfig`。

### 使用版本 2.x
<a name="driver-quickstart-nodejs.step-2-v2"></a>

------
#### [ JavaScript ]

```
var qldb = require('amazon-qldb-driver-nodejs');
var https = require('https');

function main() {
    var maxConcurrentTransactions = 10;
    var retryLimit = 4;

    var agentForQldb = new https.Agent({
        keepAlive: true,
        maxSockets: maxConcurrentTransactions
    });

    var serviceConfigurationOptions = {
        region: "us-east-1",
        httpOptions: {
            agent: agentForQldb
        }
    };

    // Use driver's default backoff function for this example (no second parameter provided to RetryConfig)
    var retryConfig = new qldb.RetryConfig(retryLimit);
    var driver = new qldb.QldbDriver("quick-start", serviceConfigurationOptions, maxConcurrentTransactions, retryConfig);
}

main();
```

------
#### [ TypeScript ]

```
import { QldbDriver, RetryConfig } from "amazon-qldb-driver-nodejs";
import { ClientConfiguration } from "aws-sdk/clients/acm";
import { Agent } from "https";

function main(): void {
    const maxConcurrentTransactions: number = 10;
    const agentForQldb: Agent = new Agent({
        keepAlive: true,
        maxSockets: maxConcurrentTransactions
    });
    const serviceConfigurationOptions: ClientConfiguration = {
        region: "us-east-1",
        httpOptions: {
            agent: agentForQldb
        }
    };
    const retryLimit: number = 4;
    // Use driver's default backoff function for this example (no second parameter provided to RetryConfig)
    const retryConfig: RetryConfig = new RetryConfig(retryLimit);
    const driver: QldbDriver = new QldbDriver("quick-start", serviceConfigurationOptions, maxConcurrentTransactions, retryConfig);
}

if (require.main === module) {
    main();
}
```

------

**注意**  
在此代码示例中，*us-east-1*替换为您创建账本 AWS 区域 的位置。
版本 2.x 引入了用于初始化 `QldbDriver` 的新可选参数 `RetryConfig`。
为简单起见，本指南中的其余代码示例使用具有默认设置的驱动程序，如以下版本 1.x 示例中所述。您也可以使用您自己的驱动程序实例来代替自定义 `RetryConfig`。
此代码示例初始化一个驱动程序，该驱动程序通过设置 keep-alive 选项来重用现有连接。要了解更多信息，请参阅 [设置建议](getting-started.nodejs.md#nodejs-setup) 了解 Node.js 驱动程序。

### 使用版本 1.x
<a name="driver-quickstart-nodejs.step-2-v1"></a>

------
#### [ JavaScript ]

```
const qldb = require('amazon-qldb-driver-nodejs');

function main() {
    // Use default settings
    const driver = new qldb.QldbDriver("quick-start");
}

main();
```

------
#### [ TypeScript ]

```
import { QldbDriver } from "amazon-qldb-driver-nodejs";

function main(): void {
    // Use default settings
    const driver: QldbDriver = new QldbDriver("quick-start");
}

if (require.main === module) {
    main();
}
```

------

**注意**  
您可以设置 `AWS_REGION` 环境变量以确定区域。有关更多信息，请参阅《*适用于 JavaScript 的 AWS SDK 开发人员指南*》中的 [设置 AWS 区域](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-region.html)。

## 第 3 步：创建表和索引
<a name="driver-quickstart-nodejs.step-3"></a>

以下代码示例显示如何运行 `CREATE TABLE` 和 `CREATE INDEX` 语句。

1. 添加以下函数，创建名为 `People` 的表。

------
#### [ JavaScript ]

   ```
   async function createTable(txn) {
       await txn.execute("CREATE TABLE People");
   }
   ```

------
#### [ TypeScript ]

   ```
   async function createTable(txn: TransactionExecutor): Promise<void> {
       await txn.execute("CREATE TABLE People");
   }
   ```

------

1. 添加以下函数，为 `People` 表中的 `firstName` 字段创建索引。[索引](ql-reference.create-index.md)是优化查询性能和帮助限制[乐观并发控制（OCC）冲突异常](concurrency.md)所必需的。

------
#### [ JavaScript ]

   ```
   async function createIndex(txn) {
       await txn.execute("CREATE INDEX ON People (firstName)");
   }
   ```

------
#### [ TypeScript ]

   ```
   async function createIndex(txn: TransactionExecutor): Promise<void> {
       await txn.execute("CREATE INDEX ON People (firstName)");
   }
   ```

------

1. 在 `main` 函数中，你先调用 `createTable`，然后调用 `createIndex`。

------
#### [ JavaScript ]

   ```
   const qldb = require('amazon-qldb-driver-nodejs');
   
   async function main() {
       // Use default settings
       const driver = new qldb.QldbDriver("quick-start");
   
       await driver.executeLambda(async (txn) => {
           console.log("Create table People");
           await createTable(txn);
           console.log("Create index on firstName");
           await createIndex(txn);
       });
   
       driver.close();
   }
   
   main();
   ```

------
#### [ TypeScript ]

   ```
   import { QldbDriver, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   
   async function main(): Promise<void> {
       // Use default settings
       const driver: QldbDriver = new QldbDriver("quick-start");
   
       await driver.executeLambda(async (txn: TransactionExecutor) => {
           console.log("Create table People");
           await createTable(txn);
           console.log("Create index on firstName");
           await createIndex(txn);
       });
   
       driver.close();
   }
   
   if (require.main === module) {
       main();
   }
   ```

------

1. 运行代码以创建表和索引。

------
#### [ JavaScript ]

   ```
   $ node app.js
   ```

------
#### [ TypeScript ]

   ```
   $ tsc app.ts; node app.js
   ```

------

## 第 4 步：插入文档
<a name="driver-quickstart-nodejs.step-4"></a>

以下代码示例显示如何运行 `INSERT` 语句。QLDB 支持 [PartiQL](ql-reference.md) 查询语言（兼容 SQL）和 [Amazon Ion](ion.md) 数据格式（JSON 的超集）。

1. 添加以下函数，在 `People` 表格中插入文档。

------
#### [ JavaScript ]

   ```
   async function insertDocument(txn) {
       const person = {
           firstName: "John",
           lastName: "Doe",
           age: 42
       };
       await txn.execute("INSERT INTO People ?", person);
   }
   ```

------
#### [ TypeScript ]

   ```
   async function insertDocument(txn: TransactionExecutor): Promise<void> {
       const person: Record<string, any> = {
           firstName: "John",
           lastName: "Doe",
           age: 42
       };
       await txn.execute("INSERT INTO People ?", person);
   }
   ```

------

   此示例使用问号（`?`）作为变量占位符，将文档信息传递给语句。该 `execute` 方法同时支持 Amazon Ion 类型和 Node.js 本机类型。
**提示**  
要使用单个 [INSERT](ql-reference.insert.md) 语句插入多个文档，可以向该语句传递一个[列表](driver-working-with-ion.md#driver-ion-list)类型的参数，如下所示。  

   ```
   // people is a list
   txn.execute("INSERT INTO People ?", people);
   ```
传递 Ion 列表时，不要将变量占位符（`?`）括在双尖括号（`<<...>>`）内。在手动 PartiQL 语句中，双尖括号表示名为*bag*的无序集合。

1. 在 `main` 函数中，移除 `createTable` 和 `createIndex` 调用，然后向添加调用 `insertDocument`。

------
#### [ JavaScript ]

   ```
   const qldb = require('amazon-qldb-driver-nodejs');
   
   async function main() {
       // Use default settings
       const driver = new qldb.QldbDriver("quick-start");
   
       await driver.executeLambda(async (txn) => {
           console.log("Insert document");
           await insertDocument(txn);
       });
   
       driver.close();
   }
   
   main();
   ```

------
#### [ TypeScript ]

   ```
   import { QldbDriver, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   
   async function main(): Promise<void> {
       // Use default settings
       const driver: QldbDriver = new QldbDriver("quick-start");
   
       await driver.executeLambda(async (txn: TransactionExecutor) => {
           console.log("Insert document");
           await insertDocument(txn);
       });
   
       driver.close();
   }
   
   if (require.main === module) {
       main();
   }
   ```

------

## 第 5 步：查询文档
<a name="driver-quickstart-nodejs.step-5"></a>

以下代码示例显示如何运行 `SELECT` 语句。

1. 添加以下函数，用于从 `People` 表格中查询文档。

------
#### [ JavaScript ]

   ```
   async function fetchDocuments(txn) {
       return await txn.execute("SELECT firstName, age, lastName FROM People WHERE firstName = ?", "John");
   }
   ```

------
#### [ TypeScript ]

   ```
   async function fetchDocuments(txn: TransactionExecutor): Promise<dom.Value[]> {
       return (await txn.execute("SELECT firstName, age, lastName FROM People WHERE firstName = ?", "John")).getResultList();
   }
   ```

------

1. 在 `main` 函数中，在对的调用 `fetchDocuments` 之后添加以下调用 `insertDocument`。

------
#### [ JavaScript ]

   ```
   const qldb = require('amazon-qldb-driver-nodejs');
   
   async function main() {
       // Use default settings
       const driver = new qldb.QldbDriver("quick-start");
   
       var resultList = await driver.executeLambda(async (txn) => {
           console.log("Insert document");
           await insertDocument(txn);
           console.log("Fetch document");
           var result = await fetchDocuments(txn);
           return result.getResultList();
       });
   
       // Pretty print the result list
       console.log("The result List is ", JSON.stringify(resultList, null, 2));
       driver.close();
   }
   
   main();
   ```

------
#### [ TypeScript ]

   ```
   import { QldbDriver, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   import { dom } from "ion-js";
   
   async function main(): Promise<void> {
       // Use default settings
       const driver: QldbDriver = new QldbDriver("quick-start");
   
       const resultList: dom.Value[] = await driver.executeLambda(async (txn: TransactionExecutor) => {
           console.log("Insert document");
           await insertDocument(txn);
           console.log("Fetch document");
           return await fetchDocuments(txn);
       });
   
       // Pretty print the result list
       console.log("The result List is ", JSON.stringify(resultList, null, 2));
       driver.close();
   }
   
   if (require.main === module) {
       main();
   }
   ```

------

## 第 6 步：更新文档
<a name="driver-quickstart-nodejs.step-6"></a>

以下代码示例显示如何运行 `UPDATE` 语句。

1. 添加以下函数，通过修改 `lastName` 到 `"Stiles"` 来更新 `People` 表中的文档。

------
#### [ JavaScript ]

   ```
   async function updateDocuments(txn) {
       await txn.execute("UPDATE People SET lastName = ? WHERE firstName = ?", "Stiles", "John");
   }
   ```

------
#### [ TypeScript ]

   ```
   async function updateDocuments(txn: TransactionExecutor): Promise<void> {
       await txn.execute("UPDATE People SET lastName = ? WHERE firstName = ?", "Stiles", "John");
   }
   ```

------

1. 在 `main` 函数中，在对的调用 `updateDocuments` 之后添加以下调用 `fetchDocuments`。然后，再次调用 `fetchDocuments` 来查看更新的结果。

------
#### [ JavaScript ]

   ```
   const qldb = require('amazon-qldb-driver-nodejs');
   
   async function main() {
       // Use default settings
       const driver = new qldb.QldbDriver("quick-start");
   
       var resultList = await driver.executeLambda(async (txn) => {
           console.log("Insert document");
           await insertDocument(txn);
           console.log("Fetch document");
           await fetchDocuments(txn);
           console.log("Update document");
           await updateDocuments(txn);
           console.log("Fetch document after update");
           var result = await fetchDocuments(txn);
           return result.getResultList();
       });
   
       // Pretty print the result list
       console.log("The result List is ", JSON.stringify(resultList, null, 2));
       driver.close();
   }
   
   main();
   ```

------
#### [ TypeScript ]

   ```
   import { QldbDriver, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   import { dom } from "ion-js";
   
   async function main(): Promise<void> {
       // Use default settings
       const driver: QldbDriver = new QldbDriver("quick-start");
   
       const resultList: dom.Value[] = await driver.executeLambda(async (txn: TransactionExecutor) => {
           console.log("Insert document");
           await insertDocument(txn);
           console.log("Fetch document");
           await fetchDocuments(txn);
           console.log("Update document");
           await updateDocuments(txn);
           console.log("Fetch document after update");
           return await fetchDocuments(txn);
       });
   
       // Pretty print the result list
       console.log("The result List is ", JSON.stringify(resultList, null, 2));
       driver.close();
   }
   
   if (require.main === module) {
       main();
   }
   ```

------

1. 运行代码以插入、查询和更新文档。

------
#### [ JavaScript ]

   ```
   $ node app.js
   ```

------
#### [ TypeScript ]

   ```
   $ tsc app.ts; node app.js
   ```

------

## 运行完整的应用程序
<a name="driver-quickstart-nodejs.complete"></a>

以下代码示例是完整版本的 `app.js` 和 `app.ts`。您还可以从头到尾运行此代码，而不必单独执行前面的步骤。此应用程序演示了对名为 `quick-start` 的分类账的一些基本的 CRUD 操作。

**注意**  
在运行此代码之前，请确保 `quick-start` 分类账中还没有名为 `People` 的活动表。

------
#### [ JavaScript ]

```
const qldb = require('amazon-qldb-driver-nodejs');

async function createTable(txn) {
    await txn.execute("CREATE TABLE People");
}

async function createIndex(txn) {
    await txn.execute("CREATE INDEX ON People (firstName)");
}

async function insertDocument(txn) {
    const person = {
        firstName: "John",
        lastName: "Doe",
        age: 42
    };
    await txn.execute("INSERT INTO People ?", person);
}

async function fetchDocuments(txn) {
    return await txn.execute("SELECT firstName, age, lastName FROM People WHERE firstName = ?", "John");
}

async function updateDocuments(txn) {
    await txn.execute("UPDATE People SET lastName = ? WHERE firstName = ?", "Stiles", "John");
}

async function main() {
    // Use default settings
    const driver = new qldb.QldbDriver("quick-start");

    var resultList = await driver.executeLambda(async (txn) => {
        console.log("Create table People");
        await createTable(txn);
        console.log("Create index on firstName");
        await createIndex(txn);
        console.log("Insert document");
        await insertDocument(txn);
        console.log("Fetch document");
        await fetchDocuments(txn);
        console.log("Update document");
        await updateDocuments(txn);
        console.log("Fetch document after update");
        var result = await fetchDocuments(txn);
        return result.getResultList();
    });

    // Pretty print the result list
    console.log("The result List is ", JSON.stringify(resultList, null, 2));
    driver.close();
}

main();
```

------
#### [ TypeScript ]

```
import { QldbDriver, TransactionExecutor } from "amazon-qldb-driver-nodejs";
import { dom } from "ion-js";

async function createTable(txn: TransactionExecutor): Promise<void> {
    await txn.execute("CREATE TABLE People");
}

async function createIndex(txn: TransactionExecutor): Promise<void> {
    await txn.execute("CREATE INDEX ON People (firstName)");
}

async function insertDocument(txn: TransactionExecutor): Promise<void> {
    const person: Record<string, any> = {
        firstName: "John",
        lastName: "Doe",
        age: 42
    };
    await txn.execute("INSERT INTO People ?", person);
}

async function fetchDocuments(txn: TransactionExecutor): Promise<dom.Value[]> {
    return (await txn.execute("SELECT firstName, age, lastName FROM People WHERE firstName = ?", "John")).getResultList();
}

async function updateDocuments(txn: TransactionExecutor): Promise<void> {
    await txn.execute("UPDATE People SET lastName = ? WHERE firstName = ?", "Stiles", "John");
};

async function main(): Promise<void> {
    // Use default settings
    const driver: QldbDriver = new QldbDriver("quick-start");

    const resultList: dom.Value[] = await driver.executeLambda(async (txn: TransactionExecutor) => {
        console.log("Create table People");
        await createTable(txn);
        console.log("Create index on firstName");
        await createIndex(txn);
        console.log("Insert document");
        await insertDocument(txn);
        console.log("Fetch document");
        await fetchDocuments(txn);
        console.log("Update document");
        await updateDocuments(txn);
        console.log("Fetch document after update");
        return await fetchDocuments(txn);
    });

    // Pretty print the result list
    console.log("The result List is ", JSON.stringify(resultList, null, 2));
    driver.close();
}

if (require.main === module) {
    main();
}
```

------

要运行完整的应用程序，请输入以下命令。

------
#### [ JavaScript ]

```
$ node app.js
```

------
#### [ TypeScript ]

```
$ tsc app.ts; node app.js
```

------

# Node.js 的 Amazon QLDB 驱动程序 — 说明书参考
<a name="driver-cookbook-nodejs"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

本参考指南显示了 Node.js 的 Amazon QLDB 驱动程序的常见用例。它提供的 JavaScript TypeScript 代码示例演示了如何使用驱动程序运行基本的*创建、读取、更新和删除* (CRUD) 操作。它还包括用于处理 Amazon Ion 数据的代码示例。此外，本指南还重点介绍了使事务具有幂等性和实现唯一性约束的最佳实践。

**Contents**
+ [

## 导入驱动程序
](#cookbook-nodejs.importing)
+ [

## 实例化驱动程序
](#cookbook-nodejs.instantiating)
+ [

## CRUD 操作
](#cookbook-nodejs.crud)
  + [

### 创建表
](#cookbook-nodejs.crud.creating-tables)
  + [

### 创建索引
](#cookbook-nodejs.crud.creating-indexes)
  + [

### 阅读文档
](#cookbook-nodejs.crud.reading)
    + [

#### 使用查询参数
](#cookbook-nodejs.reading-using-params)
  + [

### 插入文档
](#cookbook-nodejs.crud.inserting)
    + [

#### 在一条语句内插入多个文档
](#cookbook-nodejs.crud.inserting.multiple)
  + [

### 更新文档
](#cookbook-nodejs.crud.updating)
  + [

### 删除文档
](#cookbook-nodejs.crud.deleting)
  + [

### 在一个事务中运行多条语句
](#cookbook-nodejs.crud.multi-statement)
  + [

### 重试逻辑
](#cookbook-nodejs.crud.retry-logic)
  + [

### 实现唯一限制
](#cookbook-nodejs.crud.uniqueness-constraints)
+ [

## 使用 Amazon Ion
](#cookbook-nodejs.ion)
  + [

### 导入 Ion 模块
](#cookbook-nodejs.ion.import)
  + [

### 创建 Ion 类型
](#cookbook-nodejs.ion.creating-types)
  + [

### 获取 Ion 二进制转储
](#cookbook-nodejs.ion.getting-binary)
  + [

### 获取 Ion 文本转储
](#cookbook-nodejs.ion.getting-text)

## 导入驱动程序
<a name="cookbook-nodejs.importing"></a>

下面的代码示例导入驱动程序。

------
#### [ JavaScript ]

```
var qldb = require('amazon-qldb-driver-nodejs');
var ionjs = require('ion-js');
```

------
#### [ TypeScript ]

```
import { QldbDriver, TransactionExecutor } from "amazon-qldb-driver-nodejs";
import { dom, dumpBinary, load } from "ion-js";
```

------

**注意**  
此示例还导入了 Amazon Ion 软件包（`ion-js`）。在本参考中运行某些数据操作时，您需要此软件包来处理 Ion 数据。要了解更多信息，请参阅 [使用 Amazon Ion](#cookbook-nodejs.ion)。

## 实例化驱动程序
<a name="cookbook-nodejs.instantiating"></a>

以下代码示例使用默认设置创建连接到指定分类账名称的驱动程序实例。

------
#### [ JavaScript ]

```
const qldbDriver = new qldb.QldbDriver("vehicle-registration");
```

------
#### [ TypeScript ]

```
const qldbDriver: QldbDriver = new QldbDriver("vehicle-registration");
```

------

## CRUD 操作
<a name="cookbook-nodejs.crud"></a>

QLDB 作为事务的一部分运行*创建、读取、更新和删除*（CRUD）操作。

**警告**  
作为最佳实践，使写事务严格地幂等。

**使事务幂等**

我们建议将写事务设置为幂等，以避免重试时出现任何意想不到的副作用。如果事务可以运行多次并每次都产生相同的结果，则事务是*幂等的*。

例如，假设有一个事务，要将文档插入名为 `Person` 的表中。事务应该首先检查文档是否已经存在于表中。如果没有这种检查，表最终可能会有重复的文档。

假设 QLDB 在服务器端成功提交了事务，但客户端在等待响应时超时。如果事务不是幂等的，则在重试的情况下可以多次插入相同的文档。

**使用索引避免全表扫描**

我们还建议您在索引字段或文档 ID 上使用*相等*运算符来运行带有`WHERE`谓词子句的语句；例如，`WHERE indexedField = 123`或 `WHERE indexedField IN (456, 789)`。如果没有这种索引查找，QLDB 需要进行表扫描，这可能会导致事务超时或*乐观并发控制*（OCC）冲突。

有关 OCC 的更多信息，请参阅[Amazon QLDB 并发模型](concurrency.md)。

**隐式创建的事务**

[QldbDriver.executeLambd](https://amazon-qldb-docs.s3.amazonaws.com/drivers/nodejs/2.2.0/classes/_src_qldbdriver_.qldbdriver.html#executelambda) a 方法接受一个接收实例的 lambda 函数 [TransactionExecutor](https://amazon-qldb-docs.s3.amazonaws.com/drivers/nodejs/2.2.0/classes/_src_transactionexecutor_.transactionexecutor.html)，您可以使用该函数来运行语句。`TransactionExecutor` 的实例封装了隐式创建的事务。

您可以使用事务执行器的[执行](https://amazon-qldb-docs.s3.amazonaws.com/drivers/nodejs/2.2.0/classes/_src_transactionexecutor_.transactionexecutor.html#execute)方法在 lambda 函数中运行语句。当 lambda 函数返回时，驱动程序会隐式提交事务。

**注意**  
该 `execute` 方法同时支持 Amazon Ion 类型和 Node.js 本机类型。如果您将 Node.js 原生类型作为参数传递给 `execute`，则驱动程序会使用该 `ion-js` 包将其转换为 Ion 类型（前提是支持对给定 Node.js 数据类型的转换）。有关支持的数据类型和转换规则，请参阅 Ion JavaScript DOM [自述文件](https://github.com/amzn/ion-js/blob/master/src/dom/README.md)。

以下各节介绍如何运行基本的 CRUD 操作、指定自定义重试逻辑以及如何实现唯一性约束。

**Contents**
+ [

### 创建表
](#cookbook-nodejs.crud.creating-tables)
+ [

### 创建索引
](#cookbook-nodejs.crud.creating-indexes)
+ [

### 阅读文档
](#cookbook-nodejs.crud.reading)
  + [

#### 使用查询参数
](#cookbook-nodejs.reading-using-params)
+ [

### 插入文档
](#cookbook-nodejs.crud.inserting)
  + [

#### 在一条语句内插入多个文档
](#cookbook-nodejs.crud.inserting.multiple)
+ [

### 更新文档
](#cookbook-nodejs.crud.updating)
+ [

### 删除文档
](#cookbook-nodejs.crud.deleting)
+ [

### 在一个事务中运行多条语句
](#cookbook-nodejs.crud.multi-statement)
+ [

### 重试逻辑
](#cookbook-nodejs.crud.retry-logic)
+ [

### 实现唯一限制
](#cookbook-nodejs.crud.uniqueness-constraints)

### 创建表
<a name="cookbook-nodejs.crud.creating-tables"></a>

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        await txn.execute("CREATE TABLE Person");
    });
})();
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        await txn.execute('CREATE TABLE Person');
    });
}());
```

------

### 创建索引
<a name="cookbook-nodejs.crud.creating-indexes"></a>

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        await txn.execute("CREATE INDEX ON Person (GovId)");
    });
})();
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        await txn.execute('CREATE INDEX ON Person (GovId)');
    });
}());
```

------

### 阅读文档
<a name="cookbook-nodejs.crud.reading"></a>

------
#### [ JavaScript ]

```
(async function() {
    // Assumes that Person table has documents as follows:
    // { "GovId": "TOYENC486FH", "FirstName": "Brent" }
    await qldbDriver.executeLambda(async (txn) => {
        const results = (await txn.execute("SELECT * FROM Person WHERE GovId = 'TOYENC486FH'")).getResultList();
        for (let result of results) {
            console.log(result.get('GovId')); // prints [String: 'TOYENC486FH']
            console.log(result.get('FirstName')); // prints [String: 'Brent']
        }
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    // Assumes that Person table has documents as follows:
    // { "GovId": "TOYENC486FH", "FirstName": "Brent" }
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        const results: dom.Value[] = (await txn.execute("SELECT * FROM Person WHERE GovId = 'TOYENC486FH'")).getResultList();
        for (let result of results) {
            console.log(result.get('GovId')); // prints [String: 'TOYENC486FH']
            console.log(result.get('FirstName')); // prints [String: 'Brent']
        }
    });
}());
```

------

#### 使用查询参数
<a name="cookbook-nodejs.reading-using-params"></a>

以下代码示例使用原生类型查询参数。

------
#### [ JavaScript ]

```
(async function() {
    // Assumes that Person table has documents as follows:
    // { "GovId": "TOYENC486FH", "FirstName": "Brent" }
    await qldbDriver.executeLambda(async (txn) => {
        const results = (await txn.execute('SELECT * FROM Person WHERE GovId = ?', 'TOYENC486FH')).getResultList();
        for (let result of results) {
            console.log(result.get('GovId')); // prints [String: 'TOYENC486FH']
            console.log(result.get('FirstName')); // prints [String: 'Brent']
        }
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    // Assumes that Person table has documents as follows:
    // { "GovId": "TOYENC486FH", "FirstName": "Brent" }
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        const results: dom.Value[] = (await txn.execute('SELECT * FROM Person WHERE GovId = ?', 'TOYENC486FH')).getResultList();
        for (let result of results) {
            console.log(result.get('GovId')); // prints [String: 'TOYENC486FH']
            console.log(result.get('FirstName')); // prints [String: 'Brent']
        }
    });
}());
```

------

以下代码示例使用 Ion 类型查询参数。

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        const govId = ionjs.load("TOYENC486FH");

        const results = (await txn.execute('SELECT * FROM Person WHERE GovId = ?', govId)).getResultList();
        for (let result of results) {
            console.log(result.get('GovId')); // prints [String: 'TOYENC486FH']
            console.log(result.get('FirstName')); // prints [String: 'Brent']
        }
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        const govId: dom.Value = load("TOYENC486FH");

        const results: dom.Value[] = (await txn.execute('SELECT * FROM Person WHERE GovId = ?', govId)).getResultList();
        for (let result of results) {
            console.log(result.get('GovId')); // prints [String: 'TOYENC486FH']
            console.log(result.get('FirstName')); // prints [String: 'Brent']
        }
    });
}());
```

------

以下代码示例使用了多个查询参数。

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        const results = (await txn.execute('SELECT * FROM Person WHERE GovId = ? AND FirstName = ?', 'TOYENC486FH', 'Brent')).getResultList();
        for (let result of results) {
            console.log(result.get('GovId')); // prints [String: 'TOYENC486FH']
            console.log(result.get('FirstName')); // prints [String: 'Brent']
        }
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        const results: dom.Value[] = (await txn.execute('SELECT * FROM Person WHERE GovId = ? AND FirstName = ?', 'TOYENC486FH', 'Brent')).getResultList();
        for (let result of results) {
            console.log(result.get('GovId')); // prints [String: 'TOYENC486FH']
            console.log(result.get('FirstName')); // prints [String: 'Brent']
        }
    });
}());
```

------

以下代码示例使用查询参数列表。

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        const govIds = ['TOYENC486FH','LOGANB486CG','LEWISR261LL'];
        /*
        Assumes that Person table has documents as follows:
        { "GovId": "TOYENC486FH", "FirstName": "Brent" }
        { "GovId": "LOGANB486CG", "FirstName": "Brent" }
        { "GovId": "LEWISR261LL", "FirstName": "Raul" }
        */
        const results = (await txn.execute('SELECT * FROM Person WHERE GovId IN (?,?,?)', ...govIds)).getResultList();
        for (let result of results) {
            console.log(result.get('GovId'));
            console.log(result.get('FirstName'));
            /*
            prints:
            [String: 'TOYENC486FH']
            [String: 'Brent']
            [String: 'LOGANB486CG']
            [String: 'Brent']
            [String: 'LEWISR261LL']
            [String: 'Raul']
            */
        }
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        const govIds: string[] = ['TOYENC486FH','LOGANB486CG','LEWISR261LL'];
        /*
        Assumes that Person table has documents as follows:
        { "GovId": "TOYENC486FH", "FirstName": "Brent" }
        { "GovId": "LOGANB486CG", "FirstName": "Brent" }
        { "GovId": "LEWISR261LL", "FirstName": "Raul" }
        */
        const results: dom.Value[] = (await txn.execute('SELECT * FROM Person WHERE GovId IN (?,?,?)', ...govIds)).getResultList();
        for (let result of results) {
            console.log(result.get('GovId'));
            console.log(result.get('FirstName'));
            /*
            prints:
            [String: 'TOYENC486FH']
            [String: 'Brent']
            [String: 'LOGANB486CG']
            [String: 'Brent']
            [String: 'LEWISR261LL']
            [String: 'Raul']
            */
        }
    });
}());
```

------

**注意**  
当您在没有索引查找的情况下运行查询时，它会调用全表扫描。在此示例中，我们建议在`GovId`字段上设置 [索引](ql-reference.create-index.md)以优化性能。如果不开启`GovId`索引，查询可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

### 插入文档
<a name="cookbook-nodejs.crud.inserting"></a>

以下代码示例插入本地数据类型。

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        // Check if doc with GovId:TOYENC486FH exists
        // This is critical to make this transaction idempotent
        const results = (await txn.execute('SELECT * FROM Person WHERE GovId = ?', 'TOYENC486FH')).getResultList();
        // Insert the document after ensuring it doesn't already exist
        if (results.length == 0) {
            const doc = {
                'FirstName': 'Brent',
                'GovId': 'TOYENC486FH',
            };
            await txn.execute('INSERT INTO Person ?', doc);
        }
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        // Check if doc with GovId:TOYENC486FH exists
        // This is critical to make this transaction idempotent
        const results: dom.Value[] = (await txn.execute('SELECT * FROM Person WHERE GovId = ?', 'TOYENC486FH')).getResultList();
        // Insert the document after ensuring it doesn't already exist
        if (results.length == 0) {
            const doc: Record<string, string> = {
                'FirstName': 'Brent',
                'GovId': 'TOYENC486FH',
            };
            await txn.execute('INSERT INTO Person ?', doc);
        }
    });
}());
```

------

以下代码示例插入 Ion 数据类型。

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        // Check if doc with GovId:TOYENC486FH exists
        // This is critical to make this transaction idempotent
        const results = (await txn.execute('SELECT * FROM Person WHERE GovId = ?', 'TOYENC486FH')).getResultList();
        // Insert the document after ensuring it doesn't already exist
        if (results.length == 0) {
            const doc = {
                'FirstName': 'Brent',
                'GovId': 'TOYENC486FH',
            };
            // Create a sample Ion doc
            const ionDoc = ionjs.load(ionjs.dumpBinary(doc));

            await txn.execute('INSERT INTO Person ?', ionDoc);
        }
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        // Check if doc with GovId:TOYENC486FH exists
        // This is critical to make this transaction idempotent
        const results: dom.Value[] = (await txn.execute('SELECT * FROM Person WHERE GovId = ?', 'TOYENC486FH')).getResultList();
        // Insert the document after ensuring it doesn't already exist
        if (results.length == 0) {
            const doc: Record<string, string> = {
                'FirstName': 'Brent',
                'GovId': 'TOYENC486FH',
            };
            // Create a sample Ion doc
            const ionDoc: dom.Value = load(dumpBinary(doc));

            await txn.execute('INSERT INTO Person ?', ionDoc);
        }
    });
}());
```

------

此事务将文档插入 `Person` 表中。在插入之前，它首先检查文档是否已存在于表格内。**此检查使事务本质上是幂等。**即使您多次运行此事务，也不会造成任何异常副作用。

**注意**  
在此示例中，我们建议在 `GovId` 字段上设置索引以优化性能。如果不开启`GovId`索引，语句可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

#### 在一条语句内插入多个文档
<a name="cookbook-nodejs.crud.inserting.multiple"></a>

要使用单个 [INSERT](ql-reference.insert.md) 语句插入多个文档，可以向该语句传递一个[列表](driver-working-with-ion.md#driver-ion-list)类型的参数，如下所示。

```
// people is a list
txn.execute("INSERT INTO People ?", people);
```

传递 Ion 列表时，不要将变量占位符（`?`）括在双尖括号（`<<...>>`）内。在手动 PartiQL 语句中，双尖括号表示名为*bag*的无序集合。

### 更新文档
<a name="cookbook-nodejs.crud.updating"></a>

以下代码示例使用原生数据类型。

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        await txn.execute('UPDATE Person SET FirstName = ? WHERE GovId = ?', 'John', 'TOYENC486FH');
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        await txn.execute('UPDATE Person SET FirstName = ? WHERE GovId = ?', 'John', 'TOYENC486FH');
    });
}());
```

------

以下代码示例使用 Ion 数据类型。

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        const firstName = ionjs.load("John");
        const govId = ionjs.load("TOYENC486FH");

        await txn.execute('UPDATE Person SET FirstName = ? WHERE GovId = ?', firstName, govId);
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        const firstName: dom.Value = load("John");
        const govId: dom.Value = load("TOYENC486FH");

        await txn.execute('UPDATE Person SET FirstName = ? WHERE GovId = ?', firstName, govId);
    });
}());
```

------

**注意**  
在此示例中，我们建议在 `GovId` 字段上设置索引以优化性能。如果不开启`GovId`索引，语句可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

### 删除文档
<a name="cookbook-nodejs.crud.deleting"></a>

以下代码示例使用原生数据类型。

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        await txn.execute('DELETE FROM Person WHERE GovId = ?', 'TOYENC486FH');
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        await txn.execute('DELETE FROM Person WHERE GovId = ?', 'TOYENC486FH');
    });
}());
```

------

以下代码示例使用 Ion 数据类型。

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        const govId = ionjs.load("TOYENC486FH");

        await txn.execute('DELETE FROM Person WHERE GovId = ?', govId);
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        const govId: dom.Value = load("TOYENC486FH");

        await txn.execute('DELETE FROM Person WHERE GovId = ?', govId);
    });
}());
```

------

**注意**  
在此示例中，我们建议在 `GovId` 字段上设置索引以优化性能。如果不开启`GovId`索引，语句可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

### 在一个事务中运行多条语句
<a name="cookbook-nodejs.crud.multi-statement"></a>

------
#### [ TypeScript ]

```
// 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.
async function insureCar(driver: QldbDriver, vin: string): Promise<boolean> {

    return await driver.executeLambda(async (txn: TransactionExecutor) => {
        const results: dom.Value[] = (await txn.execute(
            "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin)).getResultList();

        if (results.length > 0) {
            await txn.execute(
                "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin);
            return true;
        }
        return false;
    });
};
```

------

### 重试逻辑
<a name="cookbook-nodejs.crud.retry-logic"></a>

驱动程序 `executeLambda` 方法具有内置的重试机制，如果发生可重试的异常（例如超时或 OCC 冲突），该机制可以重试事务。最大重试次数和退避策略为可配置。

默认的重试限制为`4`，默认的退避策略以毫秒[defaultBackoffFunction](https://amazon-qldb-docs.s3.amazonaws.com/drivers/nodejs/2.2.0/modules/_src_retry_defaultretryconfig_.html#defaultretryconfig)为基准。`10`您可以使用的实例为每个驱动程序实例以及每个事务设置重试配置。[RetryConfig](https://amazon-qldb-docs.s3.amazonaws.com/drivers/nodejs/2.2.0/classes/_src_retry_retryconfig_.retryconfig.html)

以下代码示例使用自定义重试限制和驱动程序实例的自定义退避策略指定重试逻辑。

------
#### [ JavaScript ]

```
var qldb = require('amazon-qldb-driver-nodejs');

// Configuring retry limit to 2
const retryConfig = new qldb.RetryConfig(2);
const qldbDriver = new qldb.QldbDriver("test-ledger", undefined, undefined, retryConfig);

// Configuring a custom backoff which increases delay by 1s for each attempt.
const customBackoff = (retryAttempt, error, transactionId) => {
    return 1000 * retryAttempt;
};

const retryConfigCustomBackoff = new qldb.RetryConfig(2, customBackoff);
const qldbDriverCustomBackoff = new qldb.QldbDriver("test-ledger", undefined, undefined, retryConfigCustomBackoff);
```

------
#### [ TypeScript ]

```
import { BackoffFunction, QldbDriver, RetryConfig } from "amazon-qldb-driver-nodejs"

// Configuring retry limit to 2
const retryConfig: RetryConfig = new RetryConfig(2);
const qldbDriver: QldbDriver = new QldbDriver("test-ledger", undefined, undefined, retryConfig);

// Configuring a custom backoff which increases delay by 1s for each attempt.
const customBackoff: BackoffFunction = (retryAttempt: number, error: Error, transactionId: string) => {
    return 1000 * retryAttempt;
};

const retryConfigCustomBackoff: RetryConfig = new RetryConfig(2, customBackoff);
const qldbDriverCustomBackoff: QldbDriver = new QldbDriver("test-ledger", undefined, undefined, retryConfigCustomBackoff);
```

------

以下代码示例使用自定义重试限制和自定义回退策略为特定 lambda 执行指定重试逻辑。此`executeLambda`配置将覆盖为驱动程序实例设置的重试逻辑。

------
#### [ JavaScript ]

```
var qldb = require('amazon-qldb-driver-nodejs');

// Configuring retry limit to 2
const retryConfig1 = new qldb.RetryConfig(2);
const qldbDriver = new qldb.QldbDriver("test-ledger", undefined, undefined, retryConfig1);

// Configuring a custom backoff which increases delay by 1s for each attempt.
const customBackoff = (retryAttempt, error, transactionId) => {
    return 1000 * retryAttempt;
};

const retryConfig2 = new qldb.RetryConfig(2, customBackoff);

// The config `retryConfig1` will be overridden by `retryConfig2`
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        await txn.execute('CREATE TABLE Person');
    }, retryConfig2);
}());
```

------
#### [ TypeScript ]

```
import { BackoffFunction, QldbDriver, RetryConfig, TransactionExecutor } from "amazon-qldb-driver-nodejs"

// Configuring retry limit to 2
const retryConfig1: RetryConfig = new RetryConfig(2);
const qldbDriver: QldbDriver = new QldbDriver("test-ledger", undefined, undefined, retryConfig1);

// Configuring a custom backoff which increases delay by 1s for each attempt.
const customBackoff: BackoffFunction = (retryAttempt: number, error: Error, transactionId: string) => {
    return 1000 * retryAttempt;
};

const retryConfig2: RetryConfig = new RetryConfig(2, customBackoff);

// The config `retryConfig1` will be overridden by `retryConfig2`
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        await txn.execute('CREATE TABLE Person');
    }, retryConfig2);
}());
```

------

### 实现唯一限制
<a name="cookbook-nodejs.crud.uniqueness-constraints"></a>

QLDB 不支持唯一索引，但您可在应用程序中实现此行为。

假设您要对 `Person` 表中的 `GovId` 字段实现唯一性约束。据此，可以编写执行以下操作的事务：

1. 断言该表中没有指定 `GovId` 的现有文档。

1. 如果断言通过，请插入文档。

如果一个竞争事务同时通过断言，则只有一个事务将成功提交。另一笔事务将失败，并显示 OCC 冲突异常。

以下代码示例显示了如何实现此唯一约束。

------
#### [ JavaScript ]

```
const govId = 'TOYENC486FH';
const document = {
    'FirstName': 'Brent',
    'GovId': 'TOYENC486FH',
};
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        // Check if doc with GovId = govId exists
        const results = (await txn.execute('SELECT * FROM Person WHERE GovId = ?', govId)).getResultList();
        // Insert the document after ensuring it doesn't already exist
        if (results.length == 0) {
            await txn.execute('INSERT INTO Person ?', document);
        }
    });
})();
```

------
#### [ TypeScript ]

```
const govId: string = 'TOYENC486FH';
const document: Record<string, string> = {
    'FirstName': 'Brent',
    'GovId': 'TOYENC486FH',
};
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        // Check if doc with GovId = govId exists
        const results: dom.Value[] = (await txn.execute('SELECT * FROM Person WHERE GovId = ?', govId)).getResultList();
        // Insert the document after ensuring it doesn't already exist
        if (results.length == 0) {
            await txn.execute('INSERT INTO Person ?', document);
        }
    });
})();
```

------

**注意**  
在此示例中，我们建议在 `GovId` 字段上设置索引以优化性能。如果不开启`GovId`索引，语句可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

## 使用 Amazon Ion
<a name="cookbook-nodejs.ion"></a>

以下各节说明了如何使用 Amazon Ion 模块处理 Ion 数据。

**Contents**
+ [

### 导入 Ion 模块
](#cookbook-nodejs.ion.import)
+ [

### 创建 Ion 类型
](#cookbook-nodejs.ion.creating-types)
+ [

### 获取 Ion 二进制转储
](#cookbook-nodejs.ion.getting-binary)
+ [

### 获取 Ion 文本转储
](#cookbook-nodejs.ion.getting-text)

### 导入 Ion 模块
<a name="cookbook-nodejs.ion.import"></a>

------
#### [ JavaScript ]

```
var ionjs = require('ion-js');
```

------
#### [ TypeScript ]

```
import { dom, dumpBinary, dumpText, load } from "ion-js";
```

------

### 创建 Ion 类型
<a name="cookbook-nodejs.ion.creating-types"></a>

以下代码示例从 Ion 文本创建 Ion 对象。

------
#### [ JavaScript ]

```
const ionText  = '{GovId: "TOYENC486FH", FirstName: "Brent"}';
const ionObj = ionjs.load(ionText);

console.log(ionObj.get('GovId')); // prints [String: 'TOYENC486FH']
console.log(ionObj.get('FirstName')); // prints [String: 'Brent']
```

------
#### [ TypeScript ]

```
const ionText: string = '{GovId: "TOYENC486FH", FirstName: "Brent"}';
const ionObj: dom.Value = load(ionText);

console.log(ionObj.get('GovId')); // prints [String: 'TOYENC486FH']
console.log(ionObj.get('FirstName')); // prints [String: 'Brent']
```

------

以下代码示例从 Node.js 字典创建 Ion 对象。

------
#### [ JavaScript ]

```
const aDict = {
    'GovId': 'TOYENC486FH',
    'FirstName': 'Brent'
};
const ionObj = ionjs.load(ionjs.dumpBinary(aDict));
console.log(ionObj.get('GovId')); // prints [String: 'TOYENC486FH']
console.log(ionObj.get('FirstName')); // prints [String: 'Brent']
```

------
#### [ TypeScript ]

```
const aDict: Record<string, string> = {
    'GovId': 'TOYENC486FH',
    'FirstName': 'Brent'
};
const ionObj: dom.Value = load(dumpBinary(aDict));
console.log(ionObj.get('GovId')); // prints [String: 'TOYENC486FH']
console.log(ionObj.get('FirstName')); // prints [String: 'Brent']
```

------

### 获取 Ion 二进制转储
<a name="cookbook-nodejs.ion.getting-binary"></a>

------
#### [ JavaScript ]

```
// ionObj is an Ion struct
console.log(ionjs.dumpBinary(ionObj).toString()); // prints 224,1,0,234,238,151,129,131,222,147,135,190,144,133,71,111,118,73,100,137,70,105,114,115,116,78,97,109,101,222,148,138,139,84,79,89,69,78,67,52,56,54,70,72,139,133,66,114,101,110,116
```

------
#### [ TypeScript ]

```
// ionObj is an Ion struct
console.log(dumpBinary(ionObj).toString()); // prints 224,1,0,234,238,151,129,131,222,147,135,190,144,133,71,111,118,73,100,137,70,105,114,115,116,78,97,109,101,222,148,138,139,84,79,89,69,78,67,52,56,54,70,72,139,133,66,114,101,110,116
```

------

### 获取 Ion 文本转储
<a name="cookbook-nodejs.ion.getting-text"></a>

------
#### [ JavaScript ]

```
// ionObj is an Ion struct
console.log(ionjs.dumpText(ionObj)); // prints {GovId:"TOYENC486FH",FirstName:"Brent"}
```

------
#### [ TypeScript ]

```
// ionObj is an Ion struct
console.log(dumpText(ionObj)); // prints {GovId:"TOYENC486FH",FirstName:"Brent"}
```

------

有关 Ion 的更多信息，请参阅上的 [Amazon Ion 文档](http://amzn.github.io/ion-docs/) GitHub。有关在 QLDB 中使用 Ion 的更多代码示例，请参阅[使用 Amazon QLDB 中的 Amazon Ion 数据类型](driver-working-with-ion.md)。

# 适用于 Python 的 Amazon QLDB 驱动程序
<a name="getting-started.python"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

要处理账本中的数据，您可以使用提供的驱动程序从 Python 应用程序连接到 Amazon QLDB。 AWS 以下主题介绍了如何开始使用适用于 Python 的 QLDB 驱动程序。

**Topics**
+ [

## 驱动程序资源
](#getting-started.python.resources)
+ [

## 先决条件
](#getting-started.python.prereqs)
+ [

## 安装
](#getting-started.python.install)
+ [快速入门教程](driver-quickstart-python.md)
+ [说明书参考](driver-cookbook-python.md)

## 驱动程序资源
<a name="getting-started.python.resources"></a>

有关 Python 驱动程序支持功能的更多信息，请参阅以下资源：
+ API 参考：[3.x](https://amazon-qldb-driver-python.readthedocs.io/en/latest/), [2.x](https://amazon-qldb-driver-python.readthedocs.io/en/v2.0.2/)
+ [驱动程序源代码 (GitHub)](https://github.com/awslabs/amazon-qldb-driver-python)
+ [示例应用程序源代码 (GitHub)](https://github.com/aws-samples/amazon-qldb-dmv-sample-python)
+ [Amazon Ion 代码示例](ion.code-examples.md)

## 先决条件
<a name="getting-started.python.prereqs"></a>

开始使用适用于 Python 的 QLDB 驱动程序之前，您必须执行以下操作:

1. 按照中的 AWS 设置说明进行操作[访问 Amazon QLDB](accessing.md)。这包括以下这些：

   1. 报名参加 AWS.

   1. 创建具有适当 QLDB 权限的用户。

   1. 授权以编程方式访问开发。

1. 从 [Python 下载](https://www.python.org/downloads/)网站下载并安装以下 Python 版本之一：
   + **3.6 或更高版本 **- 适用于 Python v3 的 QLDB 驱动程序
   + **3.4 或更高版本 **- 适用于 Python v2 的 QLDB 驱动程序

1. 设置您的 AWS 凭据和默认凭证 AWS 区域。有关说明，请参阅 适用于 Python (Boto3) 的 AWS SDK 文档中的[快速入门](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html#configuration)。

   有关可用区域的完整列表，请参阅 *AWS 一般参考* 中的 [Amazon QLDB 端点和限额](https://docs.aws.amazon.com/general/latest/gr/qldb.html)。

接下来，您可下载完整的教程示例应用程序，也可以只在 Python 项目中安装驱动程序并运行短代码示例。
+ 要在现有项目中安装 QLDB 驱动程序和 适用于 Python (Boto3) 的 AWS SDK ，请继续。[安装](#getting-started.python.install)
+ 要设置项目并运行演示分类账上基本数据事务的简短代码示例，请参阅 [快速入门教程](driver-quickstart-python.md)。
+ 要在完整的教程示例应用程序中运行更深入的数据和管理 API 操作示例，请参阅 [Python 教程](getting-started.python.tutorial.md)。

## 安装
<a name="getting-started.python.install"></a>

QLDB 支持以下驱动程序版本及其 Python 依赖项。


****  

| 驱动程序版本 | Python 版本 | 状态 | 最新发布日期 | 
| --- | --- | --- | --- | 
| [2.x](https://pypi.org/project/pyqldb/2.0.2/) | 3.4 或更高版本 | 量产版 | 2020 年 5 月 7 日 | 
| [3.x](https://pypi.org/project/pyqldb/) | 3.6 或更高版本 | 量产版 | 2021 年 10 月 28 日 | 

要使用（`pip`适用于 Python 的软件包管理器）从 PyPI 安装 QLDB 驱动程序，请在命令行输入以下命令。

------
#### [ 3.x ]

```
pip install pyqldb
```

------
#### [ 2.x ]

```
pip install pyqldb==2.0.2
```

------

安装驱动程序还会安装其依赖项，包括 [适用于 Python (Boto3) 的 AWS SDK](https://aws.amazon.com/sdk-for-python) 和 [Amazon Ion](ion.md) 软件包。

**使用驱动程序连接至分类账**

然后，您可以导入驱动程序，并使用它来连接到分类账。以下 Python 代码示例说明如何为指定的分类账名称创建会话。

------
#### [ 3.x ]

```
from pyqldb.driver.qldb_driver import QldbDriver
qldb_driver = QldbDriver(ledger_name='testLedger')

for table in qldb_driver.list_tables():
    print(table)
```

------
#### [ 2.x ]

```
from pyqldb.driver.pooled_qldb_driver import PooledQldbDriver

qldb_driver = PooledQldbDriver(ledger_name='testLedger')
qldb_session = qldb_driver.get_session()

for table in qldb_session.list_tables():
    print(table)
```

------

有关如何在分类账上运行基本数据事务的简短代码示例，请参阅 [说明书参考](driver-cookbook-python.md)。

# 适用于 Python 的 Amazon QLDB 驱动程序 — 快速入门教程
<a name="driver-quickstart-python"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在本教程中，您将学习如何使用适用于 Python 的最新版 Amazon QLDB 驱动程序来设置简单应用程序。本指南包括安装驱动程序的步骤以及*创建、读取、更新和删除*（CRUD）的基本操作的简短代码示例。有关在完整示例应用程序中演示这些操作的更深入的示例，请参阅 [Python 教程](getting-started.python.tutorial.md)。

**Topics**
+ [

## 先决条件
](#driver-quickstart-python.prereqs)
+ [

## 步骤 1：设置您的项目
](#driver-quickstart-python.step-1)
+ [

## 第 2 步：初始化驱动程序
](#driver-quickstart-python.step-2)
+ [

## 第 3 步：创建表和索引
](#driver-quickstart-python.step-3)
+ [

## 第 4 步：插入文档
](#driver-quickstart-python.step-4)
+ [

## 第 5 步：查询文档
](#driver-quickstart-python.step-5)
+ [

## 第 6 步：更新文档
](#driver-quickstart-python.step-6)
+ [

## 运行完整的应用程序
](#driver-quickstart-python.complete)

## 先决条件
<a name="driver-quickstart-python.prereqs"></a>

在开始之前，请务必执行以下操作：

1. 请为 Python 驱动程序完成（[先决条件](getting-started.python.md#getting-started.python.prereqs)如果尚未执行此操作）。这包括注册 AWS、授予开发所需的编程访问权限以及安装 Python 3.6 或更高版本。

1. 创建一个名为 `quick-start` 分类账。

   要了解如何创建分类账，请参阅*控制台入门*中的 [Amazon QLDB 分类账的基本操作](ledger-management.basics.md) 或 [第 1 步：创建新分类账](getting-started-step-1.md)。

## 步骤 1：设置您的项目
<a name="driver-quickstart-python.step-1"></a>

首先，请设置您的 Python 项目。

**注意**  
如果您使用的 IDE 具有自动执行这些设置步骤的功能，则可以直接跳到 [第 2 步：初始化驱动程序](#driver-quickstart-python.step-2)。

1. 为应用程序创建一个文件夹。

   ```
   $ mkdir myproject
   $ cd myproject
   ```

1. 要从 PyPI 安装适用于 Python 的 QLDB 驱动程序，请输入以下 `pip` 命令。

   ```
   $ pip install pyqldb
   ```

   安装驱动程序还会安装其依赖项，包括 [适用于 Python (Boto3) 的 AWS SDK](https://aws.amazon.com/sdk-for-net) 和 [Amazon Ion](ion.md) 软件包。

1. 创建名为 `app.py` 的新文件。

   然后，按以下步骤中逐步添加代码示例，尝试一些基本的 CRUD 操作。或者，您可以跳过本 step-by-step教程，改为运行[完整的应用程序](#driver-quickstart-python.complete)。

## 第 2 步：初始化驱动程序
<a name="driver-quickstart-python.step-2"></a>

初始化连接到名为 `quick-start` 的分类账的驱动程序实例。将以下代码添加到您的 `app.py`文件。

```
from pyqldb.config.retry_config import RetryConfig
from pyqldb.driver.qldb_driver import QldbDriver

# Configure retry limit to 3
retry_config = RetryConfig(retry_limit=3)

# Initialize the driver
print("Initializing the driver")
qldb_driver = QldbDriver("quick-start", retry_config=retry_config)
```

## 第 3 步：创建表和索引
<a name="driver-quickstart-python.step-3"></a>

以下代码示例显示如何运行 `CREATE TABLE` 和 `CREATE INDEX`。

添加以下代码，创建名为 `People` 的表，并为该表上的 `lastName` 字段创建索引。[索引](ql-reference.create-index.md)是优化查询性能和帮助限制[乐观并发控制（OCC）冲突异常](concurrency.md)所必需的。

```
def create_table(transaction_executor):
    print("Creating a table")
    transaction_executor.execute_statement("Create TABLE People")

def create_index(transaction_executor):
    print("Creating an index")
    transaction_executor.execute_statement("CREATE INDEX ON People(lastName)")

# Create a table
qldb_driver.execute_lambda(lambda executor: create_table(executor))

# Create an index on the table
qldb_driver.execute_lambda(lambda executor: create_index(executor))
```

## 第 4 步：插入文档
<a name="driver-quickstart-python.step-4"></a>

以下代码示例显示如何运行 `INSERT` 语句。QLDB 支持 [PartiQL](ql-reference.md) 查询语言（兼容 SQL）和 [Amazon Ion](ion.md) 数据格式（JSON 的超集）。

添加以下代码，在 `People` 表格中插入文档。

```
def insert_documents(transaction_executor, arg_1):
    print("Inserting a document")
    transaction_executor.execute_statement("INSERT INTO People ?", arg_1)

# Insert a document
doc_1 = { 'firstName': "John",
          'lastName': "Doe",
          'age': 32,
        }

qldb_driver.execute_lambda(lambda x: insert_documents(x, doc_1))
```

此示例使用问号（`?`）作为变量占位符，将文档信息传递给语句。该 `execute_statement` 方法同时支持 Amazon Ion 类型和 Python 本机类型。

**提示**  
要使用单个 [INSERT](ql-reference.insert.md) 语句插入多个文档，可以向该语句传递一个[列表](driver-working-with-ion.md#driver-ion-list)类型的参数，如下所示。  

```
# people is a list
transaction_executor.execute_statement("INSERT INTO Person ?", people)
```
传递 Ion 列表时，不要将变量占位符（`?`）括在双尖括号（`<<...>>`）内。在手动 PartiQL 语句中，双尖括号表示名为*bag*的无序集合。

## 第 5 步：查询文档
<a name="driver-quickstart-python.step-5"></a>

以下代码示例显示如何运行 `SELECT` 语句。

添加以下代码，用于从 `People` 表格中查询文档。

```
def read_documents(transaction_executor):
    print("Querying the table")
    cursor = transaction_executor.execute_statement("SELECT * FROM People WHERE lastName = ?", 'Doe')

    for doc in cursor:
        print(doc["firstName"])
        print(doc["lastName"])
        print(doc["age"])

# Query the table
qldb_driver.execute_lambda(lambda executor: read_documents(executor))
```

## 第 6 步：更新文档
<a name="driver-quickstart-python.step-6"></a>

以下代码示例显示如何运行 `UPDATE` 语句。

1. 添加以下代码，通过更新 `age` 到 `42` 来更新 `People` 表中的文档。

   ```
   def update_documents(transaction_executor, age, lastName):
       print("Updating the document")
       transaction_executor.execute_statement("UPDATE People SET age = ? WHERE lastName = ?", age, lastName)
   
   # Update the document
   age = 42
   lastName = 'Doe'
   
   qldb_driver.execute_lambda(lambda x: update_documents(x, age, lastName))
   ```

1. 再次查询表以查看更新的值。

   ```
   # Query the updated document
   qldb_driver.execute_lambda(lambda executor: read_documents(executor))
   ```

1. 要运行该应用程序，请在项目目录中输入以下命令。

   ```
   $ python app.py
   ```

## 运行完整的应用程序
<a name="driver-quickstart-python.complete"></a>

以下代码示例是 `app.py` 应用程序的完整版本。您还可以从头到尾复制并运行此代码示例，而不必单独执行前面的步骤。此应用程序演示了对名为 `quick-start` 的分类账的一些基本的 CRUD 操作。

**注意**  
在运行此代码之前，请确保 `quick-start` 分类账中还没有名为 `People` 的活动表。

```
from pyqldb.config.retry_config import RetryConfig
from pyqldb.driver.qldb_driver import QldbDriver

def create_table(transaction_executor):
    print("Creating a table")
    transaction_executor.execute_statement("CREATE TABLE People")

def create_index(transaction_executor):
    print("Creating an index")
    transaction_executor.execute_statement("CREATE INDEX ON People(lastName)")

def insert_documents(transaction_executor, arg_1):
    print("Inserting a document")
    transaction_executor.execute_statement("INSERT INTO People ?", arg_1)

def read_documents(transaction_executor):
    print("Querying the table")
    cursor = transaction_executor.execute_statement("SELECT * FROM People WHERE lastName = ?", 'Doe')
                                                                                                                                          
    for doc in cursor:
        print(doc["firstName"])
        print(doc["lastName"])
        print(doc["age"])

def update_documents(transaction_executor, age, lastName):
    print("Updating the document")
    transaction_executor.execute_statement("UPDATE People SET age = ? WHERE lastName = ?", age, lastName)

# Configure retry limit to 3
retry_config = RetryConfig(retry_limit=3)

# Initialize the driver
print("Initializing the driver")
qldb_driver = QldbDriver("quick-start", retry_config=retry_config)

# Create a table
qldb_driver.execute_lambda(lambda executor: create_table(executor))

# Create an index on the table
qldb_driver.execute_lambda(lambda executor: create_index(executor))

# Insert a document
doc_1 = { 'firstName': "John",
          'lastName': "Doe",
          'age': 32,
        }

qldb_driver.execute_lambda(lambda x: insert_documents(x, doc_1))

# Query the table
qldb_driver.execute_lambda(lambda executor: read_documents(executor))

# Update the document
age = 42
lastName = 'Doe'

qldb_driver.execute_lambda(lambda x: update_documents(x, age, lastName))

# Query the table for the updated document
qldb_driver.execute_lambda(lambda executor: read_documents(executor))
```

要运行完整的应用程序，请在项目目录中输入以下命令。

```
$ python app.py
```

# 适用于 Python 的Amazon QLDB 驱动程序 — 说明书参考
<a name="driver-cookbook-python"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

本参考指南显示了 Python 的 Amazon QLDB 驱动程序的常见用例。它提供了 Python 代码示例，演示了如何使用该驱动程序运行基本的*创建、读取、更新和删除*（CRUD）操作。它还包括用于处理 Amazon Ion 数据的代码示例。此外，本指南还重点介绍了使事务具有幂等性和实现唯一性约束的最佳实践。

**注意**  
在适用的情况下，某些用例对于 Python 的 QLDB 驱动程序的每个支持主要版本都配备不同代码示例。

**Contents**
+ [

## 导入驱动程序
](#cookbook-python.importing)
+ [

## 实例化驱动程序
](#cookbook-python.instantiating)
+ [

## CRUD 操作
](#cookbook-python.crud)
  + [

### 创建表
](#cookbook-python.crud.creating-tables)
  + [

### 创建索引
](#cookbook-python.crud.creating-indexes)
  + [

### 阅读文档
](#cookbook-python.crud.reading)
    + [

#### 使用查询参数
](#cookbook-python.reading-using-params)
  + [

### 插入文档
](#cookbook-python.crud.inserting)
    + [

#### 在一条语句内插入多个文档
](#cookbook-python.crud.inserting.multiple)
  + [

### 更新文档
](#cookbook-python.crud.updating)
  + [

### 删除文档
](#cookbook-python.crud.deleting)
  + [

### 在一个事务中运行多条语句
](#cookbook-python.crud.multi-statement)
  + [

### 重试逻辑
](#cookbook-python.crud.retry-logic)
  + [

### 实现唯一限制
](#cookbook-python.crud.uniqueness-constraints)
+ [

## 使用 Amazon Ion
](#cookbook-python.ion)
  + [

### 导入 Ion 模块
](#cookbook-python.ion.import)
  + [

### 创建 Ion 类型
](#cookbook-python.ion.creating-types)
  + [

### 获取 Ion 二进制转储
](#cookbook-python.ion.getting-binary)
  + [

### 获取 Ion 文本转储
](#cookbook-python.ion.getting-text)

## 导入驱动程序
<a name="cookbook-python.importing"></a>

下面的代码示例导入驱动程序。

------
#### [ 3.x ]

```
from pyqldb.driver.qldb_driver import QldbDriver
import amazon.ion.simpleion as simpleion
```

------
#### [ 2.x ]

```
from pyqldb.driver.pooled_qldb_driver import PooledQldbDriver
import amazon.ion.simpleion as simpleion
```

------

**注意**  
此示例还导入了 Amazon Ion 软件包（`amazon.ion.simpleion`）。在本参考中运行某些数据操作时，您需要此软件包来处理 Ion 数据。要了解更多信息，请参阅 [使用 Amazon Ion](#cookbook-python.ion)。

## 实例化驱动程序
<a name="cookbook-python.instantiating"></a>

以下代码示例使用默认设置创建连接到指定分类账名称的驱动程序实例。

------
#### [ 3.x ]

```
qldb_driver = QldbDriver(ledger_name='vehicle-registration')
```

------
#### [ 2.x ]

```
qldb_driver = PooledQldbDriver(ledger_name='vehicle-registration')
```

------

## CRUD 操作
<a name="cookbook-python.crud"></a>

QLDB 作为事务的一部分运行*创建、读取、更新和删除*（CRUD）操作。

**警告**  
作为最佳实践，使写事务严格地幂等。

**使事务幂等**

我们建议将写事务设置为幂等，以避免重试时出现任何意想不到的副作用。如果事务可以运行多次并每次都产生相同的结果，则事务是*幂等的*。

例如，假设有一个事务，要将文档插入名为 `Person` 的表中。事务应该首先检查文档是否已经存在于表中。如果没有这种检查，表最终可能会有重复的文档。

假设 QLDB 在服务器端成功提交了事务，但客户端在等待响应时超时。如果事务不是幂等的，则在重试的情况下可以多次插入相同的文档。

**使用索引避免全表扫描**

我们还建议您在索引字段或文档 ID 上使用*相等*运算符来运行带有`WHERE`谓词子句的语句；例如，`WHERE indexedField = 123`或 `WHERE indexedField IN (456, 789)`。如果没有这种索引查找，QLDB 需要进行表扫描，这可能会导致事务超时或*乐观并发控制*（OCC）冲突。

有关 OCC 的更多信息，请参阅[Amazon QLDB 并发模型](concurrency.md)。

**隐式创建的事务**

[pyqldb.driver.qldb\$1driver.execute\$1lambda](https://amazon-qldb-driver-python.readthedocs.io/en/stable/reference/driver/qldb_driver.html#pyqldb.driver.qldb_driver.QldbDriver.execute_lambda) 方法接受一个 lambda 函数，该函数接收一个 [TransactionExecutor](https://amazon-qldb-driver-python.readthedocs.io/en/stable/reference/execution/executor.html#pyqldb.execution.executor.Executor) 的实例，您可以用它来运行语句。`Executor` 的实例封装了隐式创建的事务。

您可以使用事务执行器的 [execute\$1statement](https://amazon-qldb-driver-python.readthedocs.io/en/stable/reference/execution/executor.html#pyqldb.execution.executor.Executor.execute_statement) 法在 lambda 函数中运行语句。当 lambda 函数返回时，驱动程序会隐式提交事务。

**注意**  
该 `execute_statement` 方法同时支持 Amazon Ion 类型和 Python 本机类型。如果您将 Python 本地类型作为参数传递给 `execute_statement`，则驱动程序会使用 `amazon.ion.simpleion` 模块将其转换为 Ion 类型（前提是支持对给定 Python 数据类型的转换）。有关支持的数据类型和转换规则，请参阅 [simpleion 源代码](https://ion-python.readthedocs.io/en/latest/_modules/amazon/ion/simpleion.html)。

以下各节介绍如何运行基本的 CRUD 操作、指定自定义重试逻辑以及如何实现唯一性约束。

**Contents**
+ [

### 创建表
](#cookbook-python.crud.creating-tables)
+ [

### 创建索引
](#cookbook-python.crud.creating-indexes)
+ [

### 阅读文档
](#cookbook-python.crud.reading)
  + [

#### 使用查询参数
](#cookbook-python.reading-using-params)
+ [

### 插入文档
](#cookbook-python.crud.inserting)
  + [

#### 在一条语句内插入多个文档
](#cookbook-python.crud.inserting.multiple)
+ [

### 更新文档
](#cookbook-python.crud.updating)
+ [

### 删除文档
](#cookbook-python.crud.deleting)
+ [

### 在一个事务中运行多条语句
](#cookbook-python.crud.multi-statement)
+ [

### 重试逻辑
](#cookbook-python.crud.retry-logic)
+ [

### 实现唯一限制
](#cookbook-python.crud.uniqueness-constraints)

### 创建表
<a name="cookbook-python.crud.creating-tables"></a>

```
def create_table(transaction_executor):
    transaction_executor.execute_statement("CREATE TABLE Person")

qldb_driver.execute_lambda(lambda executor: create_table(executor))
```

### 创建索引
<a name="cookbook-python.crud.creating-indexes"></a>

```
def create_index(transaction_executor):
    transaction_executor.execute_statement("CREATE INDEX ON Person(GovId)")

qldb_driver.execute_lambda(lambda executor: create_index(executor))
```

### 阅读文档
<a name="cookbook-python.crud.reading"></a>

```
# Assumes that Person table has documents as follows:
# { "GovId": "TOYENC486FH", "FirstName": "Brent" }

def read_documents(transaction_executor):
    cursor = transaction_executor.execute_statement("SELECT * FROM Person WHERE GovId = 'TOYENC486FH'")

    for doc in cursor:
        print(doc["GovId"]) # prints TOYENC486FH
        print(doc["FirstName"]) # prints Brent

qldb_driver.execute_lambda(lambda executor: read_documents(executor))
```

#### 使用查询参数
<a name="cookbook-python.reading-using-params"></a>

以下代码示例使用原生类型查询参数。

```
cursor = transaction_executor.execute_statement("SELECT * FROM Person WHERE GovId = ?", 'TOYENC486FH')
```

以下代码示例使用 Ion 类型查询参数。

```
name = ion.loads('Brent')
cursor = transaction_executor.execute_statement("SELECT * FROM Person WHERE FirstName = ?", name)
```

以下代码示例使用了多个查询参数。

```
cursor = transaction_executor.execute_statement("SELECT * FROM Person WHERE GovId = ? AND FirstName = ?", 'TOYENC486FH', "Brent")
```

以下代码示例使用查询参数列表。

```
gov_ids = ['TOYENC486FH','ROEE1','YH844']
cursor = transaction_executor.execute_statement("SELECT * FROM Person WHERE GovId IN (?,?,?)", *gov_ids)
```

**注意**  
当您在没有索引查找的情况下运行查询时，它会调用全表扫描。在此示例中，我们建议在`GovId`字段上设置 [索引](ql-reference.create-index.md)以优化性能。如果不开启`GovId`索引，查询可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

### 插入文档
<a name="cookbook-python.crud.inserting"></a>

以下代码示例插入本地数据类型。

```
def insert_documents(transaction_executor, arg_1):
    # Check if doc with GovId:TOYENC486FH exists
    # This is critical to make this transaction idempotent
    cursor = transaction_executor.execute_statement("SELECT * FROM Person WHERE GovId = ?", 'TOYENC486FH')
    # Check if there is any record in the cursor
    first_record = next(cursor, None)

    if first_record:
        # Record already exists, no need to insert
        pass
    else:
        transaction_executor.execute_statement("INSERT INTO Person ?", arg_1)

doc_1 = { 'FirstName': "Brent",
          'GovId': 'TOYENC486FH',
        }

qldb_driver.execute_lambda(lambda executor: insert_documents(executor, doc_1))
```

以下代码示例插入 Ion 数据类型。

```
def insert_documents(transaction_executor, arg_1):
    # Check if doc with GovId:TOYENC486FH exists
    # This is critical to make this transaction idempotent
    cursor = transaction_executor.execute_statement("SELECT * FROM Person WHERE GovId = ?", 'TOYENC486FH')
    # Check if there is any record in the cursor
    first_record = next(cursor, None)

    if first_record:
        # Record already exists, no need to insert
        pass
    else:
        transaction_executor.execute_statement("INSERT INTO Person ?", arg_1)

doc_1 = { 'FirstName': 'Brent',
          'GovId': 'TOYENC486FH',
        }

# create a sample Ion doc
ion_doc_1 = simpleion.loads(simpleion.dumps(doc_1)))

qldb_driver.execute_lambda(lambda executor: insert_documents(executor, ion_doc_1))
```

此事务将文档插入 `Person` 表中。在插入之前，它首先检查文档是否已存在于表格内。**此检查使事务本质上是幂等。**即使您多次运行此事务，也不会造成任何异常副作用。

**注意**  
在此示例中，我们建议在 `GovId` 字段上设置索引以优化性能。如果不开启`GovId`索引，语句可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

#### 在一条语句内插入多个文档
<a name="cookbook-python.crud.inserting.multiple"></a>

要使用单个 [INSERT](ql-reference.insert.md) 语句插入多个文档，可以向该语句传递一个[列表](driver-working-with-ion.md#driver-ion-list)类型的参数，如下所示。

```
# people is a list
transaction_executor.execute_statement("INSERT INTO Person ?", people)
```

传递 Ion 列表时，不要将变量占位符（`?`）括在双尖括号（`<<...>>`）内。在手动 PartiQL 语句中，双尖括号表示名为*bag*的无序集合。

### 更新文档
<a name="cookbook-python.crud.updating"></a>

以下代码示例使用原生数据类型。

```
def update_documents(transaction_executor, gov_id, name):
    transaction_executor.execute_statement("UPDATE Person SET FirstName = ?  WHERE GovId = ?", name, gov_id)

gov_id = 'TOYENC486FH'
name = 'John'

qldb_driver.execute_lambda(lambda executor: update_documents(executor, gov_id, name))
```

以下代码示例使用 Ion 数据类型。

```
def update_documents(transaction_executor, gov_id, name):
    transaction_executor.execute_statement("UPDATE Person SET FirstName = ? WHERE GovId = ?", name, gov_id)

# Ion datatypes
gov_id = simpleion.loads('TOYENC486FH')
name = simpleion.loads('John')

qldb_driver.execute_lambda(lambda executor: update_documents(executor, gov_id, name))
```

**注意**  
在此示例中，我们建议在 `GovId` 字段上设置索引以优化性能。如果不开启`GovId`索引，语句可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

### 删除文档
<a name="cookbook-python.crud.deleting"></a>

以下代码示例使用原生数据类型。

```
def delete_documents(transaction_executor, gov_id):
    cursor = transaction_executor.execute_statement("DELETE FROM Person WHERE GovId = ?", gov_id)

gov_id = 'TOYENC486FH'

qldb_driver.execute_lambda(lambda executor: delete_documents(executor, gov_id))
```

以下代码示例使用 Ion 数据类型。

```
def delete_documents(transaction_executor, gov_id):
    cursor = transaction_executor.execute_statement("DELETE FROM Person WHERE GovId = ?", gov_id)

# Ion datatypes
gov_id = simpleion.loads('TOYENC486FH')

qldb_driver.execute_lambda(lambda executor: delete_documents(executor, gov_id))
```

**注意**  
在此示例中，我们建议在 `GovId` 字段上设置索引以优化性能。如果不开启`GovId`索引，语句可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

### 在一个事务中运行多条语句
<a name="cookbook-python.crud.multi-statement"></a>

```
# 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.

def do_insure_car(transaction_executor, vin):
    cursor = transaction_executor.execute_statement(
        "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin)
    first_record = next(cursor, None)
    if first_record:
        transaction_executor.execute_statement(
            "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin)
        return True
    else:
        return False

def insure_car(qldb_driver, vin_to_insure):
    return qldb_driver.execute_lambda(
        lambda executor: do_insure_car(executor, vin_to_insure))
```

### 重试逻辑
<a name="cookbook-python.crud.retry-logic"></a>

驱动程序 `execute_lambda` 方法具有内置的重试机制，如果发生可重试的异常（例如超时或 OCC 冲突），该机制可以重试事务。

------
#### [ 3.x ]

最大重试次数和退避策略为可配置。

默认的重试限制为 `4`，默认的退避策略是以`10`毫秒为基数的 [Exponential Backoff and Jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)。你可以使用 pyqldb.config.retr [y\$1config 的实例为每个驱动程序实例和每个事务设置重试配置。 RetryConfig](https://amazon-qldb-driver-python.readthedocs.io/en/stable/reference/config/retry_config.html#pyqldb.config.retry_config.RetryConfig)。

以下代码示例使用自定义重试限制和驱动程序实例的自定义退避策略指定重试逻辑。

```
from pyqldb.config.retry_config import RetryConfig
from pyqldb.driver.qldb_driver import QldbDriver

# Configuring retry limit to 2
retry_config = RetryConfig(retry_limit=2)
qldb_driver = QldbDriver("test-ledger", retry_config=retry_config)

# Configuring a custom backoff which increases delay by 1s for each attempt.
def custom_backoff(retry_attempt, error, transaction_id):
    return 1000 * retry_attempt

retry_config_custom_backoff = RetryConfig(retry_limit=2, custom_backoff=custom_backoff)
qldb_driver = QldbDriver("test-ledger", retry_config=retry_config_custom_backoff)
```

以下代码示例使用自定义重试限制和自定义回退策略为特定 lambda 执行指定重试逻辑。此`execute_lambda`配置将覆盖为驱动程序实例设置的重试逻辑。

```
from pyqldb.config.retry_config import RetryConfig
from pyqldb.driver.qldb_driver import QldbDriver

# Configuring retry limit to 2
retry_config_1 = RetryConfig(retry_limit=4)
qldb_driver = QldbDriver("test-ledger", retry_config=retry_config_1)

# Configuring a custom backoff which increases delay by 1s for each attempt.
def custom_backoff(retry_attempt, error, transaction_id):
    return 1000 * retry_attempt

retry_config_2 = RetryConfig(retry_limit=2, custom_backoff=custom_backoff)

# The config `retry_config_1` will be overriden by `retry_config_2`
qldb_driver.execute_lambda(lambda txn: txn.execute_statement("CREATE TABLE Person"), retry_config_2)
```

------
#### [ 2.x ]

最大重试次数。您可以通过在初始化`PooledQldbDriver` 时设置 `retry_limit` 属性来配置重试限制。

默认重试限制为 `4`。

------

### 实现唯一限制
<a name="cookbook-python.crud.uniqueness-constraints"></a>

QLDB 不支持唯一索引，但您可在应用程序中实现此行为。

假设您要对 `Person` 表中的 `GovId` 字段实现唯一性约束。据此，可以编写执行以下操作的事务：

1. 断言该表中没有指定 `GovId` 的现有文档。

1. 如果断言通过，请插入文档。

如果一个竞争事务同时通过断言，则只有一个事务将成功提交。另一笔事务将失败，并显示 OCC 冲突异常。

以下代码示例显示了如何实现此唯一约束。

```
def insert_documents(transaction_executor, gov_id, document):
    # Check if doc with GovId = gov_id exists
    cursor = transaction_executor.execute_statement("SELECT * FROM Person WHERE GovId = ?", gov_id)
    # Check if there is any record in the cursor
    first_record = next(cursor, None)

    if first_record:
        # Record already exists, no need to insert
        pass
    else:
        transaction_executor.execute_statement("INSERT INTO Person ?", document)

qldb_driver.execute_lambda(lambda executor: insert_documents(executor, gov_id, document))
```

**注意**  
在此示例中，我们建议在 `GovId` 字段上设置索引以优化性能。如果不开启`GovId`索引，语句可能会有更长的延迟，还可能导致 OCC 冲突异常或者事务超时。

## 使用 Amazon Ion
<a name="cookbook-python.ion"></a>

以下各节说明了如何使用 Amazon Ion 模块处理 Ion 数据。

**Contents**
+ [

### 导入 Ion 模块
](#cookbook-python.ion.import)
+ [

### 创建 Ion 类型
](#cookbook-python.ion.creating-types)
+ [

### 获取 Ion 二进制转储
](#cookbook-python.ion.getting-binary)
+ [

### 获取 Ion 文本转储
](#cookbook-python.ion.getting-text)

### 导入 Ion 模块
<a name="cookbook-python.ion.import"></a>

```
import amazon.ion.simpleion as simpleion
```

### 创建 Ion 类型
<a name="cookbook-python.ion.creating-types"></a>

以下代码示例从 Ion 文本创建 Ion 对象。

```
ion_text = '{GovId: "TOYENC486FH", FirstName: "Brent"}'
ion_obj = simpleion.loads(ion_text)

print(ion_obj['GovId']) # prints TOYENC486FH
print(ion_obj['Name']) # prints Brent
```

以下代码示例从 Python `dict`创建 Ion 对象。

```
a_dict = { 'GovId': 'TOYENC486FH',
           'FirstName': "Brent"
         }
ion_obj = simpleion.loads(simpleion.dumps(a_dict))

print(ion_obj['GovId']) # prints TOYENC486FH
print(ion_obj['FirstName']) # prints Brent
```

### 获取 Ion 二进制转储
<a name="cookbook-python.ion.getting-binary"></a>

```
# ion_obj is an Ion struct
print(simpleion.dumps(ion_obj)) # b'\xe0\x01\x00\xea\xee\x97\x81\x83\xde\x93\x87\xbe\x90\x85GovId\x89FirstName\xde\x94\x8a\x8bTOYENC486FH\x8b\x85Brent'
```

### 获取 Ion 文本转储
<a name="cookbook-python.ion.getting-text"></a>

```
# ion_obj is an Ion struct
print(simpleion.dumps(ion_obj, binary=False)) # prints $ion_1_0 {GovId:'TOYENC486FH',FirstName:"Brent"}
```

有关使用 Ion 的更多信息，请参阅上的 [Amazon Ion 文档](http://amzn.github.io/ion-docs/) GitHub。有关在 QLDB 中使用 Ion 的更多代码示例，请参阅[使用 Amazon QLDB 中的 Amazon Ion 数据类型](driver-working-with-ion.md)。

# 在 Amazon QLDB 中通过驱动程序了解会话管理
<a name="driver-session-management"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

如您有使用关系数据库管理系统（RDBMS）的经验，可能已熟悉并发连接。QLDB 与传统 RDBMS 连接的概念不同，因为事务通过 HTTP 请求和响应消息运行。

在 QLDB 中，类比概念是*活跃会话*。从概念上讲，会话类似于用户登录，它管理有关您对分类账的数据事务请求的信息。活动会话是指正在积极运行事务会话。它可以是最近完成事务的会话，服务预计它将立即启动另一笔事务。QLDB 支持每个会话正在运行的事务。

每个分类账的并发活动会话限制在[Amazon QLDB 资源中的限额和限制](limits.md#limits.fixed)中定义。达到此限制后，任何尝试启动事务的会话都会导致错误（`LimitExceededException`）。

有关使用 QLDB 驱动程序在应用程序中配置会话池的最佳实践，请参阅*Amazon QLDB 驱动程序建议*中的[配置 QldbDriver 对象](driver.best-practices.md#driver.best-practices.configuring)。

**Contents**
+ [

## 会话生命周期
](#driver-session-mgmt.lifecycle)
+ [

## 会话过期
](#driver-session-mgmt.expiration)
+ [

## QLDB 驱动程序中的会话处理
](#driver-session-mgmt.handling)
  + [

### 会话池概述
](#driver-session-mgmt.pooling-overview)
  + [

### 会话池与事务逻辑
](#driver-session-mgmt.pooling-logic)
  + [

### 将会话重返数据池
](#driver-session-mgmt.pooling-return)

## 会话生命周期
<a name="driver-session-mgmt.lifecycle"></a>

以下 [QLDB 会话 API](https://docs.aws.amazon.com/qldb/latest/developerguide/API_Types_Amazon_QLDB_Session.html) 操作序列代表了 QLDB 会话的典型生命周期：

1. `StartSession`

1. `StartTransaction`

1. `ExecuteStatement`

1. `CommitTransaction`

1. 重复步骤 2—4 以启动更多事务（一次一个事务）。

1. `EndSession`

## 会话过期
<a name="driver-session-mgmt.expiration"></a>

无论会话是否在积极运行事务，QLDB 都会在总生命周期为**13–17 分钟**后过期并丢弃会话。会话丢失或受损的原因有很多，如硬件故障、网络故障或应用程序重启。QLDB 对会话强制使用最长生命周期，以确保客户端应用程序能应对会话故障。

## QLDB 驱动程序中的会话处理
<a name="driver-session-mgmt.handling"></a>

尽管您可以使用 S AWS DK 直接与 *QLDB* 会话 API 进行交互，但这会增加复杂性，需要您计算提交摘要。QLDB 使用此提交摘要确保事务完整性。我们建议不要直接与 API 交互，而是使用 QLDB 驱动程序。

该驱动程序在事务数据 API 中提供了高级抽象层。[它通过[SendCommand](https://docs.aws.amazon.com/qldb/latest/developerguide/API_QLDB-Session_SendCommand.html)管理 API 调用，简化了在账本数据上运行 PartiQL 语句的过程。](ql-reference.md)这些 API 调用需要驱动程序为您处理的多项参数，包括会话管理、事务管理以及出现错误时的重试策略。

**Topics**
+ [

### 会话池概述
](#driver-session-mgmt.pooling-overview)
+ [

### 会话池与事务逻辑
](#driver-session-mgmt.pooling-logic)
+ [

### 将会话重返数据池
](#driver-session-mgmt.pooling-return)

### 会话池概述
<a name="driver-session-mgmt.pooling-overview"></a>

在旧版本的 QLDB 驱动程序（例如 [Java](https://github.com/awslabs/amazon-qldb-driver-java/releases/tag/v1.1.0) v1.1.0）中，我们提供了驱动对象的两种实现：标准非池化`QldbDriver`和 `PooledQldbDriver`。顾名思义，`PooledQldbDriver` 维护着一个可在事务中重复使用的会话池。

根据用户反馈，开发人员更喜欢默认使用池化功能及其优势，而非使用标准驱动程序。因此，我们删除了`PooledQldbDriver`并将会话池功能移至`QldbDriver`。此更改包含在每个驱动程序的最新版本中（例如 [Java v2.0.0](https://github.com/awslabs/amazon-qldb-driver-java/releases/tag/v2.0.0)）。

该驱动程序提供了三种抽象级别：
+ **驱动程序**（池化驱动程序实现）- 顶级抽象。驱动程序维护和管理会话池。当您要求驱动程序运行事务时，驱动程序会从池中选择会话并使用它来运行事务。如果事务由于会话错误（`InvalidSessionException`）而失败，则驱动程序将使用另一会话重试该事务。从本质上讲，该驱动程序提供了完全托管会话体验。
+ **会话** - 比驱动程序抽象低一个级别。会话包含在池内，驱动程序管理会话的生命周期。如果事务失败，驱动程序会使用同一个会话、以最多指定次数重试该事务。但是，如果会话遇到错误（`InvalidSessionException`），QLDB 将在内部将其丢弃。然后，驱动程序负责从池中获取另一会话以重试该事务。
+ **事务** - 最低的抽象级别。事务包含在会话中，会话则管理事务的生命周期。如果出现错误，会话负责重试事务。该会话还确保它不泄露尚未提交或取消的未完成事务。

在每个驱动程序的最新版本，您只能在驱动程序抽象级别执行操作。您无法直接控制单个会话和事务（也就是说，没有用于手动启动新会话或事务的 API 操作）。

### 会话池与事务逻辑
<a name="driver-session-mgmt.pooling-logic"></a>

驱动程序的最新版本不再提供驱动程序对象的非池化实现。默认情况下，该 `QldbDriver` 对象管理会话池。当您调用运行事务时，驱动程序将执行以下步骤：

1. 驱动程序检查其是否达到会话池限制。如果是，驱动程序会立即抛出 `NoSessionAvailable` 异常。否则，转到下一步。

1. 驱动程序检查池中是否有可用会话。
   + 如果池中存在可用会话，则驱动程序将使用该会话来运行事务。
   + 如果池中没有可用会话，则驱动程序将创建新会话并使用该会话来运行事务。

1. 在第 2 步中驱动程序获得会话后，驱动程序将在会话实例上调用该 `execute` 操作。

1. 在会话的 `execute` 操作中，驱动程序尝试通过调用`startTransaction`来启动事务。
   + 如果会话无效，则`startTransaction`调用失败，驱动程序将返回至步骤 1。
   + 如果 `startTransaction` 调用成功，驱动程序将继续下一步。

1. 驱动程序运行您的 lambda 表达式。此 lambda 表达式可包含一个或多个用于运行 PartiQL 语句的调用。在 lambda 表达式完成运行且没有任何错误后，驱动程序将继续提交事务。

1. 事务的提交结果如下：
   + 提交成功，驱动程序将控制权返回至您的应用程序代码。
   + 因乐观并发控制（OCC）冲突，提交失败。在这种情况下，驱动程序使用相同会话重试步骤 4—6。最大重试次数可以在应用程序代码中配置。默认限制为 `4`。

**注意**  
如果`InvalidSessionException`在步骤 4—6 期间抛出，则驱动程序会将会话标记为已关闭，然后返回步骤 1 重试事务。  
如果在步骤 4—6 中引发任何其他异常，则驱动程序会检查是否可以重试该异常。如果是这样，它会重试此事务，直到达到指定的重试次数。否则，它会将异常传播（冒泡并抛出）至您的应用程序代码中。

### 将会话重返数据池
<a name="driver-session-mgmt.pooling-return"></a>

如果在活动事务过程中的任何时候发生`InvalidSessionException`，则驱动程序不会将会话返回到池中。QLDB 改为丢弃会话，驱动程序会从池内获得另一个会话。在所有其它情况下，驱动程序会将会话返回池中。

# Amazon QLDB 驱动程序建议
<a name="driver.best-practices"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

本部分介绍为任何支持的语言配置和使用 Amazon QLDB 驱动程序的最佳实践标准。所提供的代码示例专门针对 Java。

这些建议适用于大多数典型用例，但一种方法并不适合所有情况。使用您认为适合您的应用程序的以下建议。

**Topics**
+ [

## 配置 QldbDriver 对象
](#driver.best-practices.configuring)
+ [

## 重试异常
](#driver.best-practices.retrying)
+ [

## 优化性能
](#driver.best-practices.performance)
+ [

## 每笔事务运行多个语句
](#driver.best-practices.multiple-statements)

## 配置 QldbDriver 对象
<a name="driver.best-practices.configuring"></a>

该`QldbDriver`对象通过维护可跨事务重复使用的*会话*池来管理与分类账的连接。[会话](driver-session-management.md)代表与分类账的单个连接。QLDB 支持每个会话正在运行的事务。

**重要**  
对于较旧的驱动程序版本，会话池功能仍位于`PooledQldbDriver`对象中，而非`QldbDriver`。如果您使用的是以下版本之一，请在本主题的其余部分中将任何提及的 `QldbDriver` 内容替换为`PooledQldbDriver`。  


****  

| 驱动程序 | 版本 | 
| --- | --- | 
| Java | 1.1.0 或更早 | 
| .NET | 0.1.0-beta | 
| Node.js | 1.0.0-rc.1 或更早 | 
| Python | 2.0.2 或更早 | 
最新版本的驱动中已弃用该 `PooledQldbDriver`对象。建议升级至最新版本，并将的所有实例转换 `PooledQldbDriver` 为 `QldbDriver`。

**配置 QldbDriver 为全局对象**

若优化驱动程序和会话的使用，请确保您的应用程序实例中仅存在一个驱动程序的全局实例。例如，在 Java 中，你可以使用*依赖注入*框架，例如 [Spring](https://spring.io/)、[Google Guice](https://github.com/google/guice) 或[Dagger](https://dagger.dev/)。以下代码示例显示如何配置 `QldbDriver` 单例。

```
@Singleton
public QldbDriver qldbDriver (AWSCredentialsProvider credentialsProvider,
                                    @Named(LEDGER_NAME_CONFIG_PARAM) String ledgerName) {
    QldbSessionClientBuilder builder = QldbSessionClient.builder();
    if (null != credentialsProvider) {
        builder.credentialsProvider(credentialsProvider);
    }
    return QldbDriver.builder()
            .ledger(ledgerName)
            .transactionRetryPolicy(RetryPolicy
                .builder()
                .maxRetries(3)
                .build())
            .sessionClientBuilder(builder)
            .build();
}
```

**配置重试次数**

当出现常见的瞬态异常（例如`SocketTimeoutException`或`NoHttpResponseException`）时，驱动程序会自动重试事务。要设置最大重试次数，可以在创建的实例时使用 `transactionRetryPolicy` 配置对象的 `maxRetries``QldbDriver` 参数。（对于上一节中列出的较旧驱动程序版本，请使用`PooledQldbDriver`的参数`retryLimit`。）

`maxRetries` 的默认值为 `4`。

客户端错误，例如 “`InvalidParameterException` 无法重试”。当它们发生时，事务将中止，会话返回至池中，并将异常抛给驱动程序的客户端。

**配置最大并行会话与事务数**

`QldbDriver`的实例用于运行事务的最大分类账会话数由其`maxConcurrentTransactions`参数定义。（对于上一节中列出的较旧的驱动程序版本，这是由`PooledQldbDriver`的`poolLimit`参数定义的。）

此限制必须大于零且小于或等于会话客户端允许的最大打开 HTTP 连接数（由特定 AWS SDK 定义）。例如，在 Java 中，最大连接数是在[ClientConfiguration](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/ClientConfiguration.html#getMaxConnections--)对象中设置的。

的默认值`maxConcurrentTransactions`是您的 AWS SDK 的最大连接设置。

在应用程序 `QldbDriver` 中配置时，请考虑以下扩展注意事项：
+ 您的池中的会话数应始终至少与您计划同时运行的事务数量一样多。
+ 在监督线程委托给工作线程的多线程模型中，驱动程序应至少拥有与工作线程数量一样多的会话。否则，在峰值负载时，线程将排队等待可用会话。
+ 每个分类账的并发活动会话的服务限制在[Amazon QLDB 资源中的限额和限制](limits.md#limits.fixed)中定义。确保您配置的并发会话数不超过此限制，以便用于所有客户端单个分类账。

## 重试异常
<a name="driver.best-practices.retrying"></a>

重试 QLDB 事出现的异常时，请考虑以下建议。

**正在重试 OccConflictException**

当事务访问的数据自事务开始以来发生更改时，就会发生*乐观并发控制*（OCC）冲突异常。QLDB 在尝试提交事务时抛出此异常结果。驱动程序最多会根据配置重试事务`maxRetries`次数。

有关 OCC 的更多信息以及使用索引限制 OCC 冲突的最佳实践，请参阅[Amazon QLDB 并发模型](concurrency.md)。

**重试除外的其他异常 QldbDriver**

要在运行时抛出自定义的应用程序定义的异常时在驱动程序外部重试事务，您必须包装该事务。例如，在 Java 中，以下代码显示了如何使用 [Reslience4J](https://resilience4j.readme.io/) 库在 QLDB 内重试事务。

```
private final RetryConfig retryConfig = RetryConfig.custom()
        .maxAttempts(MAX_RETRIES)
        .intervalFunction(IntervalFunction.ofExponentialRandomBackoff())
        // Retry this exception
        .retryExceptions(InvalidSessionException.class, MyRetryableException.class)
        // But fail for any other type of exception extended from RuntimeException
        .ignoreExceptions(RuntimeException.class)
        .build();

// Method callable by a client
public void myTransactionWithRetries(Params params) {
    Retry retry = Retry.of("registerDriver", retryConfig);

    Function<Params, Void> transactionFunction = Retry.decorateFunction(
            retry,
            parameters ->  transactionNoReturn(params));
    transactionFunction.apply(params);
}

private Void transactionNoReturn(Params params) {
    try (driver.execute(txn -> {
            // Transaction code
        });
    }
    return null;
}
```

**注意**  
在 QLDB 驱动程序之外重试事务会使其产生乘数效应。例如，如果配置`QldbDriver` 为重试三次，并且自定义重试逻辑也重试三次，则同一事务最多可以重试九次。

**使事务幂等**

我们建议将写事务设置为幂等，以避免重试时出现任何意想不到的副作用。如果事务可以运行多次并每次都产生相同的结果，则事务是*幂等的*。

要了解更多信息，请参阅 [Amazon QLDB 并发模型](concurrency.md#concurrency.idempotent)。

## 优化性能
<a name="driver.best-practices.performance"></a>

要在使用驱动程序运行事务时优化此性能，请考虑以下注意事项：
+ 该`execute`操作始终对 QLDB 进行至少三次 `SendCommand` API 调用，包含以下命令：

  1. `StartTransaction`

  1. `ExecuteStatement`

     对于您在`execute`数据块中运行的每条 PartiQL 语句，都会调用此命令。

  1. `CommitTransaction`

  在计算应用程序的总体工作负载时，请考虑发出 API 调用的总数。
+ 一般来说，我们建议从单线程编写器开始，并通过在单个事务中批处理多个语句来优化事务。最大限度地提高事务大小、文档大小和每笔事务的文档数量的限额，如[Amazon QLDB 资源中的限额和限制](limits.md#limits.fixed)中所定义。
+ 如果批处理不足以满足大型事务负载，您可以通过添加其他编写器来尝试多线程。但是，您应该仔细考虑您的应用程序对文档和事务排序的要求，以及由此带来的额外复杂性。

## 每笔事务运行多个语句
<a name="driver.best-practices.multiple-statements"></a>

如[上一节](#driver.best-practices.performance)所述，您可为每个事务运行多个语句以优化应用程序的性能。在以下代码示例中，您可查询到一个表，然后在事务中更新该表中的文档。您可以通过向`execute`操作传递 lambda 表达式来实现此目的。

------
#### [ Java ]

```
// 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;
    });
}
```

------
#### [ .NET ]

```
// 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 async Task<bool> InsureVehicle(IAsyncQldbDriver driver, string vin)
{
    ValueFactory valueFactory = new ValueFactory();
    IIonValue ionVin = valueFactory.NewString(vin);

    return await driver.Execute(async txn =>
    {
        // Check if the vehicle is insured.
        Amazon.QLDB.Driver.IAsyncResult result = await txn.Execute(
            "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", ionVin);

        if (await result.CountAsync() > 0)
        {
            // If the vehicle is not insured, insure it.
            await txn.Execute(
                "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", ionVin);
            return true;
        }
        return false;
    });
}
```

------
#### [ Go ]

```
// 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.
func InsureCar(driver *qldbdriver.QLDBDriver, vin string) (bool, error) {
    insured, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {

        result, err := txn.Execute(
            "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin)
        if err != nil {
            return false, err
        }

        hasNext := result.Next(txn)
        if !hasNext && result.Err() != nil {
            return false, result.Err()
        }

        if hasNext {
            _, err = txn.Execute(
                "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin)
            if err != nil {
                return false, err
            }
            return true, nil
        }
        return false, nil
    })
    if err != nil {
        panic(err)
    }

    return insured.(bool), err
}
```

------
#### [ Node.js ]

```
// 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.
async function insureCar(driver: QldbDriver, vin: string): Promise<boolean> {

    return await driver.executeLambda(async (txn: TransactionExecutor) => {
        const results: dom.Value[] = (await txn.execute(
            "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin)).getResultList();

        if (results.length > 0) {
            await txn.execute(
                "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin);
            return true;
        }
        return false;
    });
};
```

------
#### [ Python ]

```
# 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.

def do_insure_car(transaction_executor, vin):
    cursor = transaction_executor.execute_statement(
        "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin)
    first_record = next(cursor, None)
    if first_record:
        transaction_executor.execute_statement(
            "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin)
        return True
    else:
        return False

def insure_car(qldb_driver, vin_to_insure):
    return qldb_driver.execute_lambda(
        lambda executor: do_insure_car(executor, vin_to_insure))
```

------

驱动程序的 `execute` 操作会隐式启动会话和该会话中的事务。您在 lambda 表达式中运行的每条语句都包含在此事务中。所有语句运行后，驱动程序将自动提交事务。如果在自动重试限制用尽后任何语句失败，则事务将会中止。

**在事务中传播异常**

当每个事务运行多个语句时，我们通常不建议您捕获并吞掉事务内的异常。

例如，在 Java 中，以下程序会捕获的任何`RuntimeException`实例，记录错误并继续。此代码示例被认为是错误做法，因为即使`UPDATE`语句失败，事务也会成功。因此，客户端可能会假定更新成功，而更新却没有成功。

**警告**  
切勿使用此代码示例。提供它是为了展示被认为是不良做法的反模式示例。

```
// DO NOT USE this code example because it is considered bad practice
public static void main(final String... args) {
    ConnectToLedger.getDriver().execute(txn -> {
        final Result selectTableResult = txn.execute("SELECT * FROM Vehicle WHERE VIN ='123456789'");
        // Catching an error inside the transaction is an anti-pattern because the operation might
        // not succeed.
        // In this example, the transaction succeeds even when the update statement fails.
        // So, the client might assume that the update succeeded when it didn't.
        try {
            processResults(selectTableResult);
            String model = // some code that extracts the model
            final Result updateResult = txn.execute("UPDATE Vehicle SET model = ? WHERE VIN = '123456789'",
                    Constants.MAPPER.writeValueAsIonValue(model));
        } catch (RuntimeException e) {
            log.error("Exception when updating the Vehicle table {}", e.getMessage());
        }
    });
    log.info("Vehicle table updated successfully.");
}
```

改为传播（冒泡）异常。如果事务的任何部分失败，则让`execute`操作中止事务，以便客户端可以相应地处理异常。

# 使用 Amazon QLDB 中的驱动程序了解重试策略
<a name="driver-retry-policy"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

Amazon QLDB 驱动程序使用重试策略，通过透明重试失败的事务来处理暂时异常。这些异常（例如`CapacityExceededException`和`RateExceededException`）通常会在一段时间后自行纠正。如果在适当延迟之后重试因异常而失败的事务，则很可能会成功。这有助于提高使用 QLDB 应用程序的稳定性。

**Topics**
+ [

## 可重试错误类型
](#driver-retry-policy.retryable-errors)
+ [

## 默认重试策略
](#driver-retry-policy.default)

## 可重试错误类型
<a name="driver-retry-policy.retryable-errors"></a>

当且仅当该事务的操作期间出现以下任何异常时，驱动程序才会自动重试此事务：
+ [CapacityExceededException](driver-errors.md)— 当请求超出账本的处理能力时返回。
+ [InvalidSessionException](driver-errors.md)— 当会话不再有效或会话不存在时返回。
+ [LimitExceededException](driver-errors.md)— 如果超过资源限制（例如活动会话数），则返回。
+ [OccConflictException](concurrency.md)— 当由于*乐观并发控制* (OCC) 的验证阶段失败而导致交易无法写入日志时返回。
+ [RateExceededException](driver-errors.md)— 当请求速率超过允许的吞吐量时返回。

## 默认重试策略
<a name="driver-retry-policy.default"></a>

重试策略由重试条件与退避策略组成。重试条件定义何时应重试事务，而退避策略定义在重试事务之前的等待时间。

创建驱动程序实例时，默认重试策略指定最多重试四次，并使用指数退避策略。指数退避策略使用最小延迟 10 毫秒，最大延迟 5000 毫秒，抖动相等。如果无法在重试策略内成功提交此事务，我们建议您改时尝试该事务。

指数回退的原理是对于连续错误响应，重试等待间隔越来越长。有关更多信息，请参阅 AWS 博客文章[指数退缩和抖动。](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)

# Amazon QLDB 驱动程序中的常见错误
<a name="driver-errors"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

本节描述了 Amazon QLDB 驱动程序在与[ QLDB 会话 API](https://docs.aws.amazon.com/qldb/latest/developerguide/API_Types_Amazon_QLDB_Session.html) 交互时可能引发的运行时错误。

下面是驱动程序返回的常见异常列表。每个异常都包括特定的错误消息，然后是简短的描述和可能的解决方案建议。<a name="driver-errors.varlist"></a>

**CapacityExceededException**  
消息：容量已超出  
Amazon QLDB 拒绝了该请求，因为它超出了分类账的处理能力。QLDB 对每个分类账强制执行内部扩展限制，以维护服务的运行状况和性能。此限制因每个请求的工作负载大小而异。例如，如果请求执行效率低下的数据事务，例如由非索引限定查询产生的表扫描，则该请求的工作负载可能会增加。  
我们建议您在重试请求之前等待。如果您的应用程序一直遇到此异常，请优化您的语句并降低发送到分类账的请求的速率和数量。语句优化的示例包括每个事务运行更少的语句和调优表索引。要了解如何优化语句和避免表扫描，请参阅 [优化查询性能](working.optimize.md)。  
建议使用最新版本的 QLDB 驱动程序。驱动程序具有默认的重试策略，该策略使用[指数回退和抖动](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)来自动重试此类异常。指数回退的原理是对于连续错误响应，重试等待间隔越来越长。

**InvalidSessionException**  
消息：交易*transactionId*已过期  
事务已超过其最长生命周期。在提交之前，事务最多可运行 30 秒。超过此时间限制后，QLDB 会拒绝在事务完成的任何工作，并丢弃运行该事务会话。此限制通过启动事务（而不提交或取消事务）以保护客户端免遭泄漏会话。  
如果这是应用程序中常见的异常，那么很可能是事务运行时间过长。如果事务运行时间超过 30 秒，请优化您的语句以加快事务速度。语句优化的示例包括每个事务运行更少的语句和调优表索引。有关更多信息，请参阅 [优化查询性能](working.optimize.md)。

**InvalidSessionException**  
消息：会话*sessionId*已过期  
QLDB 放弃了会话，因为它已超过其最大总生命周期。无论事务是否处于活动状态，QLDB 都会在 13-17 分钟后丢弃会话。会话丢失或受损的原因有很多，如硬件故障、网络故障或应用程序重启。因此，QLDB 对会话强制使用最长生命周期，以确保客户端软件能应对会话故障。  
如果您遇到此异常，我们建议您获取一个新的会话并重试该事务。我们还建议使用最新版本的 QLDB 驱动程序，该驱动程序代表应用程序管理会话池及其运行状况。

**InvalidSessionException**  
消息：没有这样的会话  
客户试图使用不存在的会话与 QLDB 进行事务处理。假设客户端正在使用先前存在的会话，则由于以下原因之一，该会话可能不再存在：  
+ 如果会话涉及内部服务器故障（即 HTTP 响应代码 500 错误），那么 QLDB 可能会选择完全放弃该会话，而不是允许客户使用状态不确定的会话进行事务。然后，对该会话的任何重试尝试都会失败，并出现此错误。
+ 过期的会话最终会被 QLDB 遗忘。然后，任何继续使用会话的尝试都会导致此错误，而不是初始 `InvalidSessionException`。
如果您遇到此异常，我们建议您获取一个新的会话并重试该事务。我们还建议使用最新版本的 QLDB 驱动程序，该驱动程序代表应用程序管理会话池及其运行状况。

**RateExceededException**  
消息：已超出速率  
QLDB 根据调用者的标识对客户端进行节流。QLDB 使用[令牌桶](https://en.wikipedia.org/wiki/Token_bucket)节流算法在每个区域、每个账户的基础上强制执行节流。QLDB 这样做是为了帮助提高服务的性能，并确保所有 QLDB 客户的公平使用。例如，尝试使用 `StartSessionRequest` 操作获取大量并发会话可能会导致节流。  
为了保持应用程序的运行状况并缓解进一步的节流，您可以使用[指数回退](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)和抖动来重试此异常。指数回退的原理是对于连续错误响应，重试等待间隔越来越长。建议使用最新版本的 QLDB 驱动程序。驱动程序具有默认的重试策略，该策略使用指数回退和抖动来自动重试此类异常。  
如果您的应用程序持续受到 QLDB 的 `StartSessionRequest` 调用节流，那么最新版本的 QLDB 驱动程序也可以提供帮助。驱动程序维护着一个跨事务重复使用的会话池，这有助于减少应用程序发出的 `StartSessionRequest` 调用次数。要请求提高 API 节流限额，请联系 [AWS 支持 中心](https://console.aws.amazon.com/support/home#/)。

**LimitExceededException**  
消息：已超出会话限制  
分类账的活跃会话数量超过了其配额（也称为*限制*）。此配额在 [Amazon QLDB 资源中的限额和限制](limits.md) 中定义。分类账的活动会话计数最终是一致的，并且始终在配额附近运行的分类账可能会定期看到此异常。  
为了保持应用程序的运行状况，我们建议重试此异常。为避免出现这种异常，请确保在所有客户端上为单个分类账配置的并发会话不超过 1,500 个。例如，您可以使用适用于 Java 的 [Amazon QLDB 驱动程序的[maxConcurrentTransactions](https://github.com/awslabs/amazon-qldb-driver-java/blob/master/src/main/java/software/amazon/qldb/QldbDriverBuilder.java#L125)](https://github.com/awslabs/amazon-qldb-driver-java/)方法来配置驱动程序实例中的最大可用会话数。

**QldbClientException**  
消息：流结果仅在父事务打开时有效  
事务已关闭，无法用于从 QLDB 检索结果。事务在提交或取消时关闭。  
当客户端直接处理 `Transaction` 对象，并且在提交或取消事务后尝试从 QLDB 检索结果时，就会发生此异常。为了缓解此问题，客户端必须在关闭事务之前读取数据。

# 使用示例应用程序教程，开始使用 Amazon QLDB
<a name="getting-started-sample-app"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在本教程中，您将使用 Amazon QLDB 驱动程序和软件开发工具包来创建 QLDB 账本，并在其中 AWS 填充示例数据。该驱动程序允许您的应用程序通过事务数据 API 与 QLDB 交互。该 AWS 软件开发工具包支持与 QLDB 资源管理 API 的交互。

您在此场景中创建的示例分类账是一个机动车辆部门（DMV）数据库，用于追踪有关车辆登记的完整历史信息。以下主题说明了如何添加车辆登记、修改车辆登记，以及如何查看这些登记的更改历史记录。本指南还向您展示了如何以加密方式验证注册文档，最后还会清理资源并删除示例分类账。

示例应用程序可提供以下编程语言版本。

**Topics**
+ [Java 教程](getting-started.java.tutorial.md)
+ [Node.js 教程](getting-started.nodejs.tutorial.md)
+ [Python 教程](getting-started.python.tutorial.md)

# Amazon QLDB Java 教程
<a name="getting-started.java.tutorial"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在本教程示例应用程序的本实现中，您将使用带有 Amazon QLDB 驱动程序来创建 QLDB 账本并使用示例数据填充 适用于 Java 的 AWS SDK 该账本。

在学习本教程时，您可以参考 [适用于 Java 的 AWS SDK API 参考](https://docs.aws.amazon.com/sdk-for-java/latest/reference/) 以了解管理 API 操作。有关事务数据操作，您可参见[适用于 Java 的 QLDB 驱动程序 API 参考](https://javadoc.io/doc/software.amazon.qldb/amazon-qldb-driver-java/latest/index.html)。

**注意**  
在适用的情况下，某些用例对于 Java 的 QLDB 驱动程序的每个支持主要版本都配备不同的命令或代码示例。

**Topics**
+ [

# 安装 Amazon QLDB Java 示例应用程序
](sample-app.java.md)
+ [

# 步骤 1：创建新分类账
](getting-started.java.step-1.md)
+ [

# 第 2 步：测试与分类账的连接
](getting-started.java.step-2.md)
+ [

# 步骤 3：创建表、索引与示例数据
](getting-started.java.step-3.md)
+ [

# 步骤 4：查询分类账中的表
](getting-started.java.step-4.md)
+ [

# 第 5 步：修改分类账中的文档
](getting-started.java.step-5.md)
+ [

# 步骤 6：查看文档修订历史记录
](getting-started.java.step-6.md)
+ [

# 第 7 步：验证分类账中的文档
](getting-started.java.step-7.md)
+ [

# 步骤 8：导出并验证分类账中的日记账数据
](getting-started.java.step-8.md)
+ [

# 步骤 9：（可选）清除资源
](getting-started.java.step-9.md)

# 安装 Amazon QLDB Java 示例应用程序
<a name="sample-app.java"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

本节介绍如何为 Java 教程安装和运行所提供的 Amazon QLDB 示例应用程序。 step-by-step此示例应用程序的用例是机动车辆部门（DMV）数据库，用于追踪有关车辆登记的完整历史信息。

适用于 Java 的 DMV 示例应用程序在 GitHub 存储库 a [ws-samples/-amazon-qldb-dmv-sample](https://github.com/aws-samples/amazon-qldb-dmv-sample-java) java 中是开源的。

## 先决条件
<a name="sample-app.java.prereqs"></a>

在开始之前，请确保您已完成适用于 Java[先决条件](getting-started.java.md#getting-started.java.prereqs) 的 QLDB 驱动程序。这包括以下这些：

1. 注册 AWS.

1. 创建具有适当 QLDB 权限的用户。要完成本教程中的所有步骤，您需要通过 QLDB API 对分类账资源拥有完全管理权限。

1. 如果您使用的是以外的 IDE AWS Cloud9，请安装 Java 并授予开发所需的编程访问权限。

## 安装
<a name="sample-app.java.install"></a>

以下步骤介绍如何在本地开发环境中下载与设置示例应用程序。或者，您可以使用 AWS Cloud9 作为 IDE 自动设置示例应用程序，并使用 CloudFormation 模板来配置开发资源。

### 本地开发环境
<a name="sample-app.java.local-ide"></a>

这些说明描述了如何利用自有资源和开发环境下载和安装 QLDB Java 示例应用程序。

**下载并运行示例应用程序**

1. 输入以下命令以从中克隆示例应用程序 GitHub。

------
#### [ 2.x ]

   ```
   git clone https://github.com/aws-samples/amazon-qldb-dmv-sample-java.git
   ```

------
#### [ 1.x ]

   ```
   git clone -b v1.2.0 https://github.com/aws-samples/amazon-qldb-dmv-sample-java.git
   ```

------

   此软件包包含来自[Java 教程](getting-started.java.tutorial.md)的 Gradle 配置和完整代码。

1. 下载并运行提供的应用程序。
   + 如果您使用的是 Eclipse：

     1. 启动 Eclipse，然后在**Eclipse**菜单选择**文件**、**导入**，然后选择 **现有 Gradle 项目**。

     1. 在项目根目录中，浏览并选择包含 `build.gradle` 文件的应用程序目录。然后，选择 **完成** 以使用默认 Gradle 设置进行导入。

     1. 你可以尝试运行 `ListLedgers` 程序作为示例。打开 `ListLedgers.java` 文件的上下文菜单（右键单击），选择 **作为 Java 应用程序运行**。
   + 如果您使用的是 IntelliJ：

     1. 启动 IntelliJ，在 **IntelliJ** 菜单选择 **文件**，然后选择 **打开**。

     1. 在项目根目录中，浏览并选择包含 `build.gradle`文件的应用程序目录。然后选择 **OK**（确定）。保留默认设置，然后再次选择 **确定**。

     1. 你可以尝试运行 `ListLedgers` 程序作为示例。打开`ListLedgers.java`文件的上下文（右键单击）菜单，然后选择 **“运行ListLedgers”**。

1. 继续 [步骤 1：创建新分类账](getting-started.java.step-1.md) 开始教程并创建分类账。

### AWS Cloud9
<a name="sample-app.java.cfn-ac9"></a>

这些说明描述了如何使用[AWS Cloud9](https://aws.amazon.com/cloud9)作为 IDE 自动设置适用于 Java 的 Amazon QLDB 车辆登记示例应用程序。在本指南中，您将使 用[CloudFormation](https://aws.amazon.com/cloudformation) 模板配置您的开发资源。

有关的更多信息 AWS Cloud9，请参阅《[AWS Cloud9 用户指南》](https://docs.aws.amazon.com/cloud9/latest/user-guide/)。要了解有关 CloudFormation的更多信息，请参阅 [AWS CloudFormation 用户指南](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/)。

**Topics**
+ [

#### 第 1 部分：配置资源
](#sample-app.java.cfn-ac9.step-1)
+ [

#### 部分 2：设置 IDE
](#sample-app.java.cfn-ac9.step-2)
+ [

#### 第 3 部分：运行 QLDB DMV 示例应用程序
](#sample-app.java.cfn-ac9.step-3)

#### 第 1 部分：配置资源
<a name="sample-app.java.cfn-ac9.step-1"></a>

在第一步中，您将使用 CloudFormation Amazon QLDB 示例应用程序预配置设置开发环境所需的资源。

**打开 CloudFormation 控制台并加载 QLDB 示例应用程序模板**

1. 登录 AWS 管理控制台 并在 [https://console.aws.amazon.com/cloudformat](https://console.aws.amazon.com/cloudformation/) ion 上打开 CloudFormation 控制台。

   切换至支持 QLDB 的区域。有关完整列表，请参阅 *AWS 一般参考* 中的 [Amazon QLDB 端点和限额](https://docs.aws.amazon.com/general/latest/gr/qldb.html)。以下屏幕截图 AWS 管理控制台 显示美国东部（弗吉尼亚北部）为选定区域 AWS 区域。  
![\[AWS 管理控制台 将美国东部（弗吉尼亚北部）显示为选中 AWS 区域。\]](http://docs.aws.amazon.com/zh_cn/qldb/latest/developerguide/images/cfn-ac9/aws-region-us-east-1.png)

1. 在 CloudFormation 控制台上，选择**创建堆栈**，然后选择**使用新资源（标准）**。

1. 在 **创建堆栈**页面上，选择 **指定模板**，选择 **Amazon S3 URL**。

1. 输入以下 URL，然后选择 **下一步**。

   ```
   https://amazon-qldb-assets.s3.amazonaws.com/templates/QLDB-DMV-SampleApp.yml
   ```

1. 输入**堆栈名称**（例如**qldb-sample-app**），然后选择**下一步**。

1. 您可以根据需要添加任何标签，并保留默认选项。然后选择**下一步**。

1. 检查您的堆栈设置，然后选择**创建堆栈**。 CloudFormation 脚本可能需要几分钟才能完成。

   此脚本为您的 AWS Cloud9 环境预置一个关联的亚马逊弹性计算云 (Amazon EC2) 实例，用于运行本教程中的 QLDB 示例应用程序。它还会将 [aws-samples/ amazon-qldb-dmv-sample-java](https://github.com/aws-samples/amazon-qldb-dmv-sample-java/) 存储库从您的开发环境中克隆到您的开发环境中。 GitHub AWS Cloud9 

#### 部分 2：设置 IDE
<a name="sample-app.java.cfn-ac9.step-2"></a>

在此步骤中，您已完成云开发环境设置。您可以下载并运行提供的 shell 脚本，使用示例应用程序的依赖项来设置 AWS Cloud9 IDE。

**设置您的 AWS Cloud9 环境**

1. 打开 AWS Cloud9 控制台，网址为[https://console.aws.amazon.com/cloud9/](https://console.aws.amazon.com/cloud9/)。

1. 在 **您的环境**，定位名为 **QLDB DMV Sample Application**的环境牌，然后选择 **打开 IDE**。底层 EC2 实例启动时，您的环境可能需要一分钟才能加载。

   您的 AWS Cloud9 环境已预先配置了运行本教程所需的系统依赖项。在控制台的 **环境** 导航窗格中，确认您看到一个名为`QLDB DMV Sample Application`的文件夹。 AWS Cloud9 控制台的以下屏幕截图显示了 QLDB DMV 示例应用程序环境文件夹窗格。  
![\[AWS Cloud9 控制台显示 QLDB DMV 示例应用程序环境文件夹窗格。\]](http://docs.aws.amazon.com/zh_cn/qldb/latest/developerguide/images/cfn-ac9/cloud9-folders.png)

   如果您没有看到导航窗格，请切换主机左侧的 **环境** 选项卡。如果您在窗格中看不到任何文件夹，请使用设置图标（![\[Settings icon\]](http://docs.aws.amazon.com/zh_cn/qldb/latest/developerguide/images/settings.png)）启用**显示环境根目录**。

1. 在控制台底部窗格中，您应该会看到一个打开的`bash`终端窗口。如果您没有看到这个，请从主机顶部的 **窗口 菜单中**选择**新建终端**。

1. 接下来，下载并运行安装脚本以安装 OpenJDK 8，如果适用，请从 Git 存储库中查看相应的分支。在上一步中创建的 AWS Cloud9 终端中，按顺序运行以下两个命令：

------
#### [ 2.x ]

   ```
   aws s3 cp s3://amazon-qldb-assets/setup-scripts/dmv-setup-v2.sh .
   ```

   ```
   sh dmv-setup-v2.sh
   ```

------
#### [ 1.x ]

   ```
   aws s3 cp s3://amazon-qldb-assets/setup-scripts/dmv-setup.sh .
   ```

   ```
   sh dmv-setup.sh
   ```

------

   完成后，您可看到终端中打印了以下消息：

   ```
   ** DMV Sample App setup completed , enjoy!! **
   ```

1. 花点时间浏览中的示例应用程序代码 AWS Cloud9，尤其是在以下目录路径中：`src/main/java/software/amazon/qldb/tutorial`。

#### 第 3 部分：运行 QLDB DMV 示例应用程序
<a name="sample-app.java.cfn-ac9.step-3"></a>

在本步骤中，您将学习如何使用运行 Amazon QLDB DMV 示例应用程序任务。 AWS Cloud9要运行示例代码，请返回 AWS Cloud9 终端或按照*第 2 部分：设置 IDE* 中所做的那样创建一个新的终端窗口。

**运行示例应用程序**

1. 在终端中运行以下命令，以切换至项目根目录：

   ```
   cd ~/environment/amazon-qldb-dmv-sample-java
   ```

   确保您在以下目录路径运行示例。

   ```
   /home/ec2-user/environment/amazon-qldb-dmv-sample-java/
   ```

1. 以下命令显示了运行每项任务的 Gradle 语法。

   ```
   ./gradlew run -Dtutorial=Task
   ```

   例如，运行以下命令列出您 AWS 账户 和当前区域中的所有账本。

   ```
   ./gradlew run -Dtutorial=ListLedgers
   ```

1. 继续 [步骤 1：创建新分类账](getting-started.java.step-1.md) 开始教程并创建分类账。

1. （可选）完成教程后，如果您不再需要 CloudFormation 资源，就可以清理它们了。

   1. 在 [https://console.aws.amazon.com/cloudformation](https://console.aws.amazon.com/cloudformation/) 上打开 CloudFormation 控制台，然后删除您在*第 1 部分：配置*资源中创建的堆栈。

   1. 同时删除 CloudFormation 模板为您创建的 AWS Cloud9 堆栈。

# 步骤 1：创建新分类账
<a name="getting-started.java.step-1"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在此步骤中，您将创建一个名为 `vehicle-registration` 的新 Amazon QLDB 分类账。

**创建新分类账**

1. 查看以下文件（`Constants.java`），其包含本教程中所有其他程序使用的常量值。

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazon.ion.IonSystem;
   import com.amazon.ion.system.IonSystemBuilder;
   import com.fasterxml.jackson.databind.SerializationFeature;
   import com.fasterxml.jackson.dataformat.ion.IonObjectMapper;
   import com.fasterxml.jackson.dataformat.ion.ionvalue.IonValueMapper;
   
   /**
    * Constant values used throughout this tutorial.
    */
   public final class Constants {
       public static final int RETRY_LIMIT = 4;
       public static final String LEDGER_NAME = "vehicle-registration";
       public static final String STREAM_NAME = "vehicle-registration-stream";
       public static final String VEHICLE_REGISTRATION_TABLE_NAME = "VehicleRegistration";
       public static final String VEHICLE_TABLE_NAME = "Vehicle";
       public static final String PERSON_TABLE_NAME = "Person";
       public static final String DRIVERS_LICENSE_TABLE_NAME = "DriversLicense";
       public static final String VIN_INDEX_NAME = "VIN";
       public static final String PERSON_GOV_ID_INDEX_NAME = "GovId";
       public static final String VEHICLE_REGISTRATION_LICENSE_PLATE_NUMBER_INDEX_NAME = "LicensePlateNumber";
       public static final String DRIVER_LICENSE_NUMBER_INDEX_NAME = "LicenseNumber";
       public static final String DRIVER_LICENSE_PERSONID_INDEX_NAME = "PersonId";
       public static final String JOURNAL_EXPORT_S3_BUCKET_NAME_PREFIX = "qldb-tutorial-journal-export";
       public static final String USER_TABLES = "information_schema.user_tables";
       public static final String LEDGER_NAME_WITH_TAGS = "tags";
       public static final IonSystem SYSTEM = IonSystemBuilder.standard().build();
       public static final IonObjectMapper MAPPER = new IonValueMapper(SYSTEM);
   
       private Constants() { }
   
       static {
           MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
       }
   }
   ```

------
#### [ 1.x ]

**重要**  
对于 Amazon Ion 软件包，您必须在应用程序中使用命名空间`com.amazon.ion`。 适用于 Java 的 AWS SDK 依赖于命名空间下的另一个 Ion 包`software.amazon.ion`，但这是一个与 QLDB 驱动程序不兼容的旧包。

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazon.ion.IonSystem;
   import com.amazon.ion.system.IonSystemBuilder;
   import com.fasterxml.jackson.databind.SerializationFeature;
   import com.fasterxml.jackson.dataformat.ion.IonObjectMapper;
   import com.fasterxml.jackson.dataformat.ion.ionvalue.IonValueMapper;
   
   /**
    * Constant values used throughout this tutorial.
    */
   public final class Constants {
       public static final int RETRY_LIMIT = 4;
       public static final String LEDGER_NAME = "vehicle-registration";
       public static final String STREAM_NAME = "vehicle-registration-stream";
       public static final String VEHICLE_REGISTRATION_TABLE_NAME = "VehicleRegistration";
       public static final String VEHICLE_TABLE_NAME = "Vehicle";
       public static final String PERSON_TABLE_NAME = "Person";
       public static final String DRIVERS_LICENSE_TABLE_NAME = "DriversLicense";
       public static final String VIN_INDEX_NAME = "VIN";
       public static final String PERSON_GOV_ID_INDEX_NAME = "GovId";
       public static final String VEHICLE_REGISTRATION_LICENSE_PLATE_NUMBER_INDEX_NAME = "LicensePlateNumber";
       public static final String DRIVER_LICENSE_NUMBER_INDEX_NAME = "LicenseNumber";
       public static final String DRIVER_LICENSE_PERSONID_INDEX_NAME = "PersonId";
       public static final String JOURNAL_EXPORT_S3_BUCKET_NAME_PREFIX = "qldb-tutorial-journal-export";
       public static final String USER_TABLES = "information_schema.user_tables";
       public static final String LEDGER_NAME_WITH_TAGS = "tags";
       public static final IonSystem SYSTEM = IonSystemBuilder.standard().build();
       public static final IonObjectMapper MAPPER = new IonValueMapper(SYSTEM);
   
       private Constants() { }
   
       static {
           MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
       }
   }
   ```

------
**注意**  
该 `Constants` 类包括开源 Jackson `IonValueMapper`类实例。在执行读写事务时，您可以使用此映射器处理您的 [Amazon Ion](ion.md) 数据。

   该 `CreateLedger.java` 文件还依赖于以下程序（`DescribeLedger.java`），该程序描述了分类账的当前状态。

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazonaws.services.qldb.AmazonQLDB;
   import com.amazonaws.services.qldb.model.DescribeLedgerRequest;
   import com.amazonaws.services.qldb.model.DescribeLedgerResult;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   /**
    * Describe a QLDB ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class DescribeLedger {
       public static AmazonQLDB client = CreateLedger.getClient();
       public static final Logger log = LoggerFactory.getLogger(DescribeLedger.class);
   
       private DescribeLedger() { }
   
       public static void main(final String... args) {
           try {
   
               describe(Constants.LEDGER_NAME);
   
           } catch (Exception e) {
               log.error("Unable to describe a ledger!", e);
           }
       }
   
       /**
        * Describe a ledger.
        *
        * @param name
        *              Name of the ledger to describe.
        * @return {@link DescribeLedgerResult} from QLDB.
        */
       public static DescribeLedgerResult describe(final String name) {
           log.info("Let's describe ledger with name: {}...", name);
           DescribeLedgerRequest request = new DescribeLedgerRequest().withName(name);
           DescribeLedgerResult result = client.describeLedger(request);
           log.info("Success. Ledger description: {}", result);
           return result;
       }
   }
   ```

1. 编译并运行 `CreateLedger.java` 程序以创建名为`vehicle-registration`的分类账。

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazonaws.client.builder.AwsClientBuilder;
   import com.amazonaws.services.qldb.AmazonQLDB;
   import com.amazonaws.services.qldb.AmazonQLDBClientBuilder;
   import com.amazonaws.services.qldb.model.CreateLedgerRequest;
   import com.amazonaws.services.qldb.model.CreateLedgerResult;
   import com.amazonaws.services.qldb.model.DescribeLedgerResult;
   import com.amazonaws.services.qldb.model.LedgerState;
   import com.amazonaws.services.qldb.model.PermissionsMode;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   /**
    * Create a ledger and wait for it to be active.
    * <p>
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class CreateLedger {
       public static final Logger log = LoggerFactory.getLogger(CreateLedger.class);
       public static final Long LEDGER_CREATION_POLL_PERIOD_MS = 10_000L;
       public static String endpoint = null;
       public static String region = null;
       public static AmazonQLDB client = getClient();
   
       private CreateLedger() {
       }
   
       /**
        * Build a low-level QLDB client.
        *
        * @return {@link AmazonQLDB} control plane client.
        */
       public static AmazonQLDB getClient() {
           AmazonQLDBClientBuilder builder = AmazonQLDBClientBuilder.standard();
           if (null != endpoint && null != region) {
               builder.setEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endpoint, region));
           }
           return builder.build();
       }
   
       public static void main(final String... args) throws Exception {
           try {
               client = getClient();
   
               create(Constants.LEDGER_NAME);
   
               waitForActive(Constants.LEDGER_NAME);
   
           } catch (Exception e) {
               log.error("Unable to create the ledger!", e);
               throw e;
           }
       }
   
       /**
        * Create a new ledger with the specified ledger name.
        *
        * @param ledgerName Name of the ledger to be created.
        * @return {@link CreateLedgerResult} from QLDB.
        */
       public static CreateLedgerResult create(final String ledgerName) {
           log.info("Let's create the ledger with name: {}...", ledgerName);
           CreateLedgerRequest request = new CreateLedgerRequest()
                   .withName(ledgerName)
                   .withPermissionsMode(PermissionsMode.ALLOW_ALL);
           CreateLedgerResult result = client.createLedger(request);
           log.info("Success. Ledger state: {}.", result.getState());
           return result;
       }
   
       /**
        * Wait for a newly created ledger to become active.
        *
        * @param ledgerName Name of the ledger to wait on.
        * @return {@link DescribeLedgerResult} from QLDB.
        * @throws InterruptedException if thread is being interrupted.
        */
       public static DescribeLedgerResult waitForActive(final String ledgerName) throws InterruptedException {
           log.info("Waiting for ledger to become active...");
           while (true) {
               DescribeLedgerResult result = DescribeLedger.describe(ledgerName);
               if (result.getState().equals(LedgerState.ACTIVE.name())) {
                   log.info("Success. Ledger is active and ready to use.");
                   return result;
               }
               log.info("The ledger is still creating. Please wait...");
               Thread.sleep(LEDGER_CREATION_POLL_PERIOD_MS);
           }
       }
   }
   ```

------
#### [ 1.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazonaws.services.qldb.AmazonQLDB;
   import com.amazonaws.services.qldb.AmazonQLDBClientBuilder;
   import com.amazonaws.services.qldb.model.CreateLedgerRequest;
   import com.amazonaws.services.qldb.model.CreateLedgerResult;
   import com.amazonaws.services.qldb.model.DescribeLedgerResult;
   import com.amazonaws.services.qldb.model.LedgerState;
   import com.amazonaws.services.qldb.model.PermissionsMode;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   /**
    * Create a ledger and wait for it to be active.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class CreateLedger {
       public static final Logger log = LoggerFactory.getLogger(CreateLedger.class);
       public static final Long LEDGER_CREATION_POLL_PERIOD_MS = 10_000L;
       public static AmazonQLDB client = getClient();
   
       private CreateLedger() { }
   
       /**
        * Build a low-level QLDB client.
        *
        * @return {@link AmazonQLDB} control plane client.
        */
       public static AmazonQLDB getClient() {
           return AmazonQLDBClientBuilder.standard().build();
       }
   
       public static void main(final String... args) throws Exception {
           try {
   
               create(Constants.LEDGER_NAME);
   
               waitForActive(Constants.LEDGER_NAME);
   
           } catch (Exception e) {
               log.error("Unable to create the ledger!", e);
               throw e;
           }
       }
   
       /**
        * Create a new ledger with the specified ledger name.
        *
        * @param ledgerName
        *              Name of the ledger to be created.
        * @return {@link CreateLedgerResult} from QLDB.
        */
       public static CreateLedgerResult create(final String ledgerName) {
           log.info("Let's create the ledger with name: {}...", ledgerName);
           CreateLedgerRequest request = new CreateLedgerRequest()
                   .withName(ledgerName)
                   .withPermissionsMode(PermissionsMode.ALLOW_ALL);
           CreateLedgerResult result = client.createLedger(request);
           log.info("Success. Ledger state: {}.", result.getState());
           return result;
       }
   
       /**
        * Wait for a newly created ledger to become active.
        *
        * @param ledgerName
        *              Name of the ledger to wait on.
        * @return {@link DescribeLedgerResult} from QLDB.
        * @throws InterruptedException if thread is being interrupted.
        */
       public static DescribeLedgerResult waitForActive(final String ledgerName) throws InterruptedException {
           log.info("Waiting for ledger to become active...");
           while (true) {
               DescribeLedgerResult result = DescribeLedger.describe(ledgerName);
               if (result.getState().equals(LedgerState.ACTIVE.name())) {
                   log.info("Success. Ledger is active and ready to use.");
                   return result;
               }
               log.info("The ledger is still creating. Please wait...");
               Thread.sleep(LEDGER_CREATION_POLL_PERIOD_MS);
           }
       }
   }
   ```

------
**注意**  
在 `createLedger` 调用中，您必须指定分类账名称和权限模式。我们强烈建议使用 `STANDARD` 权限模式来最大限度地提高分类账数据的安全性。
创建分类账时，将默认启用 *删除保护*。QLDB 中有一项功能，可防止分类账被任何用户删除。您可以选择使用 QLDB API 或 AWS Command Line Interface () 在创建账本时禁用删除保护。AWS CLI
您还可选择指定要附加到分类账的标签。

要验证您与新分类账的连接，请继续 [第 2 步：测试与分类账的连接](getting-started.java.step-2.md)。

# 第 2 步：测试与分类账的连接
<a name="getting-started.java.step-2"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在此步骤中，您将验证是否可以使用事务数据 API 端点连接至 Amazon QLDB 中的`vehicle-registration`分类账。

**测试分类账的连接性**

1. 查看以下程序（`ConnectToLedger.java`），该程序创建了与`vehicle-registration` 分类账的数据会话连接。

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import java.net.URI;
   import java.net.URISyntaxException;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
   import software.amazon.awssdk.services.qldbsession.QldbSessionClient;
   import software.amazon.awssdk.services.qldbsession.QldbSessionClientBuilder;
   import software.amazon.qldb.QldbDriver;
   import software.amazon.qldb.RetryPolicy;
   
   /**
    * Connect to a session for a given ledger using default settings.
    * <p>
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class ConnectToLedger {
       public static final Logger log = LoggerFactory.getLogger(ConnectToLedger.class);
       public static AwsCredentialsProvider credentialsProvider;
       public static String endpoint = null;
       public static String ledgerName = Constants.LEDGER_NAME;
       public static String region = null;
       public static QldbDriver driver;
   
       private ConnectToLedger() {
       }
   
       /**
        * Create a pooled driver for creating sessions.
        *
        * @param retryAttempts How many times the transaction will be retried in
        * case of a retryable issue happens like Optimistic Concurrency Control exception,
        * server side failures or network issues.
        * @return The pooled driver for creating sessions.
        */
       public static QldbDriver createQldbDriver(int retryAttempts) {
           QldbSessionClientBuilder builder = getAmazonQldbSessionClientBuilder();
           return QldbDriver.builder()
                                .ledger(ledgerName)
                                .transactionRetryPolicy(RetryPolicy
                                      .builder()
                                      .maxRetries(retryAttempts)
                                      .build())
                                .sessionClientBuilder(builder)
                                .build();
       }
   
       /**
        * Create a pooled driver for creating sessions.
        *
        * @return The pooled driver for creating sessions.
        */
       public static QldbDriver createQldbDriver() {
           QldbSessionClientBuilder builder = getAmazonQldbSessionClientBuilder();
           return QldbDriver.builder()
               .ledger(ledgerName)
               .transactionRetryPolicy(RetryPolicy.builder()
                                                  .maxRetries(Constants.RETRY_LIMIT).build())
               .sessionClientBuilder(builder)
               .build();
       }
   
       /**
        * Creates a QldbSession builder that is passed to the QldbDriver to connect to the Ledger.
        *
        * @return An instance of the AmazonQLDBSessionClientBuilder
        */
       public static QldbSessionClientBuilder getAmazonQldbSessionClientBuilder() {
           QldbSessionClientBuilder builder = QldbSessionClient.builder();
           if (null != endpoint && null != region) {
               try {
                   builder.endpointOverride(new URI(endpoint));
               } catch (URISyntaxException e) {
                   throw new IllegalArgumentException(e);
               }
           }
           if (null != credentialsProvider) {
               builder.credentialsProvider(credentialsProvider);
           }
           return builder;
       }
   
       /**
        * Create a pooled driver for creating sessions.
        *
        * @return The pooled driver for creating sessions.
        */
       public static QldbDriver getDriver() {
           if (driver == null) {
               driver = createQldbDriver();
           }
           return driver;
       }
   
   
       public static void main(final String... args) {
           Iterable<String> tables = ConnectToLedger.getDriver().getTableNames();
           log.info("Existing tables in the ledger:");
           for (String table : tables) {
               log.info("- {} ", table);
           }
       }
   }
   ```

**注意**  
若对分类账运行数据操作，您必须创建`QldbDriver`类实例以连接至特定分类账。这与您在上一步中创建分类账时使用的`AmazonQLDB` 客户端对象不同。此前客户端仅适用于运行[Amazon QLDB API 参考](api-reference.md)中列出的管理 API 操作。
首先，创建一个 `QldbDriver` 对象。创建此驱动程序时，您必须指定分类账名称。  
然后，您可以使用此驱动程序的 `execute` 方法运行 PartiQL 语句。
或者，您可指定事务异常的最大重试次数。该 `execute` 方法会自动重试乐观并发控制（OCC）冲突和其他常见的瞬态异常，但不超过此可配置限制。默认值为 `4`。  
如果在达到限制后事务仍失败，则驱动程序会抛出异常。要了解更多信息，请参阅 [使用 Amazon QLDB 中的驱动程序了解重试策略](driver-retry-policy.md)。

------
#### [ 1.x ]

   ```
   /*
    * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazonaws.auth.AWSCredentialsProvider;
   import com.amazonaws.client.builder.AwsClientBuilder;
   import com.amazonaws.services.qldbsession.AmazonQLDBSessionClientBuilder;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   import software.amazon.qldb.PooledQldbDriver;
   import software.amazon.qldb.QldbDriver;
   import software.amazon.qldb.QldbSession;
   import software.amazon.qldb.exceptions.QldbClientException;
   
   /**
    * Connect to a session for a given ledger using default settings.
    * <p>
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class ConnectToLedger {
       public static final Logger log = LoggerFactory.getLogger(ConnectToLedger.class);
       public static AWSCredentialsProvider credentialsProvider;
       public static String endpoint = null;
       public static String ledgerName = Constants.LEDGER_NAME;
       public static String region = null;
       private static PooledQldbDriver driver;
   
       private ConnectToLedger() {
       }
   
       /**
        * Create a pooled driver for creating sessions.
        *
        * @return The pooled driver for creating sessions.
        */
       public static PooledQldbDriver createQldbDriver() {
           AmazonQLDBSessionClientBuilder builder = AmazonQLDBSessionClientBuilder.standard();
           if (null != endpoint && null != region) {
               builder.setEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endpoint, region));
           }
           if (null != credentialsProvider) {
               builder.setCredentials(credentialsProvider);
           }
           return PooledQldbDriver.builder()
                   .withLedger(ledgerName)
                   .withRetryLimit(Constants.RETRY_LIMIT)
                   .withSessionClientBuilder(builder)
                   .build();
       }
   
       /**
        * Create a pooled driver for creating sessions.
        *
        * @return The pooled driver for creating sessions.
        */
       public static PooledQldbDriver getDriver() {
           if (driver == null) {
               driver = createQldbDriver();
           }
           return driver;
       }
   
       /**
        * Connect to a ledger through a {@link QldbDriver}.
        *
        * @return {@link QldbSession}.
        */
       public static QldbSession createQldbSession() {
           return getDriver().getSession();
       }
   
       public static void main(final String... args) {
           try (QldbSession qldbSession = createQldbSession()) {
               log.info("Listing table names ");
               for (String tableName : qldbSession.getTableNames()) {
                   log.info(tableName);
               }
           } catch (QldbClientException e) {
               log.error("Unable to create session.", e);
           }
       }
   }
   ```

**注意**  
要对分类账运行数据操作，您必须创建`PooledQldbDriver` 或 `QldbDriver` 类的实例以连接到特定分类账。这与您在上一步中创建分类账时使用的 `AmazonQLDB` 客户端对象不同。之前的客户端仅用于[Amazon QLDB API 参考](api-reference.md)中列出的管理 API 操作。  
除非您需要使用`QldbDriver`实现自定义会话池，否则我们建议使用`PooledQldbDriver`。默认`PooledQldbDriver`池大小是会话客户端允许[的最大打开 HTTP 连接数](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/ClientConfiguration.html#getMaxConnections--)。
首先，创建一个 `PooledQldbDriver` 对象。创建此驱动程序时，您必须指定分类账名称。  
然后，您可以使用此驱动程序的 `execute` 方法运行 PartiQL 语句。或者，您可通过此池驱动程序对象手动创建会话并使用该会话`execute`方法。会话代表与分类账的单个连接。
或者，您可指定事务异常的最大重试次数。该 `execute` 方法会自动重试乐观并发控制（OCC）冲突和其他常见的瞬态异常，但不超过此可配置限制。默认值为 `4`。  
如果在达到限制后事务仍失败，则驱动程序会抛出异常。要了解更多信息，请参阅 [使用 Amazon QLDB 中的驱动程序了解重试策略](driver-retry-policy.md)。

------

1. 编译并运行该 `ConnectToLedger.java` 程序，以测试您的数据会话与`vehicle-registration` 分类账的连接。

**覆盖 AWS 区域**

示例应用程序以 AWS 区域默认方式连接到 QLDB，您可以按照先决条件步骤中的说明进行设置。[设置您的默认 AWS 凭证和区域](getting-started.java.md#getting-started.java.credentials)可以修改 QLDB 会话客户构建程序的属性来更改区域。

------
#### [ 2.x ]

下面的代码示例实例化一个新的 `QldbSessionClientBuilder` 对象。

```
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.qldbsession.QldbSessionClientBuilder;

// This client builder will default to US East (Ohio)
QldbSessionClientBuilder builder = QldbSessionClient.builder()
    .region(Region.US_EAST_2);
```

您可以使用 `region` 方法对任何可用区域中的 QLDB 运行您的代码。有关完整列表，请参阅 *AWS 一般参考* 中的 [Amazon QLDB 端点和限额](https://docs.aws.amazon.com/general/latest/gr/qldb.html)。

------
#### [ 1.x ]

下面的代码示例实例化一个新的 `AmazonQLDBSessionClientBuilder` 对象。

```
import com.amazonaws.regions.Regions;
import com.amazonaws.services.qldbsession.AmazonQLDBSessionClientBuilder;

// This client builder will default to US East (Ohio)
AmazonQLDBSessionClientBuilder builder = AmazonQLDBSessionClientBuilder.standard()
    .withRegion(Regions.US_EAST_2);
```

您可以使用 `withRegion` 方法对任何可用区域中的 QLDB 运行您的代码。有关完整列表，请参阅 *AWS 一般参考* 中的 [Amazon QLDB 端点和限额](https://docs.aws.amazon.com/general/latest/gr/qldb.html)。

------

要在 `vehicle-registration` 分类账中创建表，请继续 [步骤 3：创建表、索引与示例数据](getting-started.java.step-3.md)。

# 步骤 3：创建表、索引与示例数据
<a name="getting-started.java.step-3"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

当您的 Amazon QLDB 分类账处于活动状态且接受连接时，您可开始创建有关车辆、车主和注册信息的数据表。创建表和索引后，可以向其中加载数据。

在此步骤中，您将在`vehicle-registration`分类账中创建四个表格：
+ `VehicleRegistration`
+ `Vehicle`
+ `Person`
+ `DriversLicense`

您还可创建以下索引。


****  

| 表名称 | 字段 | 
| --- | --- | 
| VehicleRegistration | VIN | 
| VehicleRegistration | LicensePlateNumber | 
| Vehicle | VIN | 
| Person | GovId | 
| DriversLicense | LicenseNumber | 
| DriversLicense | PersonId | 

插入示例数据时，首先要在 `Person` 表格中插入文档。然后使用系统分配的、来自每个`Person`文档的`id`填充适当`VehicleRegistration` 和 `DriversLicense`文档中的相应文档。

**提示**  
最佳做法是使用系统分配的 `id` 文档作为外键。虽然您可以定义作为唯一标识符的字段（例如车辆的 VIN），但文档的真正唯一标识符是`id`。字段都包含在文档元数据中，您可以在*提交视图*（系统定义的表格视图）中对其进行查询。  
有关 QLDB 中的视图的更多信息，请参阅[核心概念](ledger-structure.md)。了解有关元数据的更多信息，请参阅 [查询文档元数据](working.metadata.md)。

**设置示例数据**

1. 审查以下 `.java` 文件。这些模型类代表您存储在 `vehicle-registration` 表格中的文档。它们可在 Amazon Ion 格式之间进行序列化。
**注意**  
[Amazon QLDB 文档](ql-reference.docs.md)以 Ion 格式（JSON 父集）存储。因此，您可使用 FasterXML Jackson 库对 JSON 中的数据进行建模。

   1. `DriversLicense.java`

      ```
      /*
       * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.model;
      
      import com.fasterxml.jackson.annotation.JsonCreator;
      import com.fasterxml.jackson.annotation.JsonProperty;
      import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
      import com.fasterxml.jackson.databind.annotation.JsonSerialize;
      import software.amazon.qldb.tutorial.model.streams.RevisionData;
      
      import java.time.LocalDate;
      
      /**
       * Represents a driver's license, serializable to (and from) Ion.
       */
      public final class DriversLicense implements RevisionData {
          private final String personId;
          private final String licenseNumber;
          private final String licenseType;
      
          @JsonSerialize(using = IonLocalDateSerializer.class)
          @JsonDeserialize(using = IonLocalDateDeserializer.class)
          private final LocalDate validFromDate;
      
          @JsonSerialize(using = IonLocalDateSerializer.class)
          @JsonDeserialize(using = IonLocalDateDeserializer.class)
          private final LocalDate validToDate;
      
          @JsonCreator
          public DriversLicense(@JsonProperty("PersonId") final String personId,
                                @JsonProperty("LicenseNumber") final String licenseNumber,
                                @JsonProperty("LicenseType") final String licenseType,
                                @JsonProperty("ValidFromDate") final LocalDate validFromDate,
                                @JsonProperty("ValidToDate") final LocalDate validToDate) {
              this.personId = personId;
              this.licenseNumber = licenseNumber;
              this.licenseType = licenseType;
              this.validFromDate = validFromDate;
              this.validToDate = validToDate;
          }
      
          @JsonProperty("PersonId")
          public String getPersonId() {
              return personId;
          }
      
          @JsonProperty("LicenseNumber")
          public String getLicenseNumber() {
              return licenseNumber;
          }
      
          @JsonProperty("LicenseType")
          public String getLicenseType() {
              return licenseType;
          }
      
          @JsonProperty("ValidFromDate")
          public LocalDate getValidFromDate() {
              return  validFromDate;
          }
      
          @JsonProperty("ValidToDate")
          public LocalDate getValidToDate() {
              return  validToDate;
          }
      
          @Override
          public String toString() {
              return "DriversLicense{" +
                      "personId='" + personId + '\'' +
                      ", licenseNumber='" + licenseNumber + '\'' +
                      ", licenseType='" + licenseType + '\'' +
                      ", validFromDate=" + validFromDate +
                      ", validToDate=" + validToDate +
                      '}';
          }
      }
      ```

   1. `Person.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.model;
      
      import java.time.LocalDate;
      
      import com.fasterxml.jackson.annotation.JsonCreator;
      import com.fasterxml.jackson.annotation.JsonProperty;
      import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
      import com.fasterxml.jackson.databind.annotation.JsonSerialize;
      
      import software.amazon.qldb.TransactionExecutor;
      import software.amazon.qldb.tutorial.Constants;
      import software.amazon.qldb.tutorial.model.streams.RevisionData;
      
      /**
       * Represents a person, serializable to (and from) Ion.
       */ 
      public final class Person implements RevisionData {
          private final String firstName;
          private final String lastName;
      
          @JsonSerialize(using = IonLocalDateSerializer.class)
          @JsonDeserialize(using = IonLocalDateDeserializer.class)
          private final LocalDate dob;
          private final String govId;
          private final String govIdType;
          private final String address;
      
          @JsonCreator
          public Person(@JsonProperty("FirstName") final String firstName,
                        @JsonProperty("LastName") final String lastName,
                        @JsonProperty("DOB") final LocalDate dob,
                        @JsonProperty("GovId") final String govId,
                        @JsonProperty("GovIdType") final String govIdType,
                        @JsonProperty("Address") final String address) {
              this.firstName = firstName;
              this.lastName = lastName;
              this.dob = dob;
              this.govId = govId;
              this.govIdType = govIdType;
              this.address = address;
          }
      
          @JsonProperty("Address")
          public String getAddress() {
              return address;
          }
      
          @JsonProperty("DOB")
          public LocalDate getDob() {
              return dob;
          }
      
          @JsonProperty("FirstName")
          public String getFirstName() {
              return firstName;
          }
      
          @JsonProperty("LastName")
          public String getLastName() {
              return lastName;
          }
      
          @JsonProperty("GovId")
          public String getGovId() {
              return govId;
          }
      
          @JsonProperty("GovIdType")
          public String getGovIdType() {
              return govIdType;
          }
      
          /**
           * This returns the unique document ID given a specific government ID.
           *
           * @param txn
           *              A transaction executor object.
           * @param govId
           *              The government ID of a driver.
           * @return the unique document ID.
           */
          public static String getDocumentIdByGovId(final TransactionExecutor txn, final String govId) {
              return SampleData.getDocumentId(txn, Constants.PERSON_TABLE_NAME, "GovId", govId);
          }
      
          @Override
          public String toString() {
              return "Person{" +
                      "firstName='" + firstName + '\'' +
                      ", lastName='" + lastName + '\'' +
                      ", dob=" + dob +
                      ", govId='" + govId + '\'' +
                      ", govIdType='" + govIdType + '\'' +
                      ", address='" + address + '\'' +
                      '}';
          }
      }
      ```

   1. `VehicleRegistration.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.model;
      
      import com.fasterxml.jackson.annotation.JsonCreator;
      import com.fasterxml.jackson.annotation.JsonProperty;
      import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
      import com.fasterxml.jackson.databind.annotation.JsonSerialize;
      import software.amazon.qldb.TransactionExecutor;
      import software.amazon.qldb.tutorial.Constants;
      import software.amazon.qldb.tutorial.model.streams.RevisionData;
      
      import java.math.BigDecimal;
      import java.time.LocalDate;
      
      /**
       * Represents a vehicle registration, serializable to (and from) Ion.
       */ 
      public final class VehicleRegistration implements RevisionData {
      
          private final String vin;
          private final String licensePlateNumber;
          private final String state;
          private final String city;
          private final BigDecimal pendingPenaltyTicketAmount;
          private final LocalDate validFromDate;
          private final LocalDate validToDate;
          private final Owners owners;
      
          @JsonCreator
          public VehicleRegistration(@JsonProperty("VIN") final String vin,
                                     @JsonProperty("LicensePlateNumber") final String licensePlateNumber,
                                     @JsonProperty("State") final String state,
                                     @JsonProperty("City") final String city,
                                     @JsonProperty("PendingPenaltyTicketAmount") final BigDecimal pendingPenaltyTicketAmount,
                                     @JsonProperty("ValidFromDate") final LocalDate validFromDate,
                                     @JsonProperty("ValidToDate") final LocalDate validToDate,
                                     @JsonProperty("Owners") final Owners owners) {
              this.vin = vin;
              this.licensePlateNumber = licensePlateNumber;
              this.state = state;
              this.city = city;
              this.pendingPenaltyTicketAmount = pendingPenaltyTicketAmount;
              this.validFromDate = validFromDate;
              this.validToDate = validToDate;
              this.owners = owners;
          }
      
          @JsonProperty("City")
          public String getCity() {
              return city;
          }
      
          @JsonProperty("LicensePlateNumber")
          public String getLicensePlateNumber() {
              return licensePlateNumber;
          }
      
          @JsonProperty("Owners")
          public Owners getOwners() {
              return owners;
          }
      
          @JsonProperty("PendingPenaltyTicketAmount")
          public BigDecimal getPendingPenaltyTicketAmount() {
              return pendingPenaltyTicketAmount;
          }
      
          @JsonProperty("State")
          public String getState() {
              return state;
          }
      
          @JsonProperty("ValidFromDate")
          @JsonSerialize(using = IonLocalDateSerializer.class)
          @JsonDeserialize(using = IonLocalDateDeserializer.class)
          public LocalDate getValidFromDate() {
              return validFromDate;
          }
      
          @JsonProperty("ValidToDate")
          @JsonSerialize(using = IonLocalDateSerializer.class)
          @JsonDeserialize(using = IonLocalDateDeserializer.class)
          public LocalDate getValidToDate() {
              return validToDate;
          }
      
          @JsonProperty("VIN")
          public String getVin() {
              return vin;
          }
      
          /**
           * Returns the unique document ID of a vehicle given a specific VIN.
           *
           * @param txn
           *              A transaction executor object.
           * @param vin
           *              The VIN of a vehicle.
           * @return the unique document ID of the specified vehicle.
           */
          public static String getDocumentIdByVin(final TransactionExecutor txn, final String vin) {
              return SampleData.getDocumentId(txn, Constants.VEHICLE_REGISTRATION_TABLE_NAME, "VIN", vin);
          }
      
          @Override
          public String toString() {
              return "VehicleRegistration{" +
                      "vin='" + vin + '\'' +
                      ", licensePlateNumber='" + licensePlateNumber + '\'' +
                      ", state='" + state + '\'' +
                      ", city='" + city + '\'' +
                      ", pendingPenaltyTicketAmount=" + pendingPenaltyTicketAmount +
                      ", validFromDate=" + validFromDate +
                      ", validToDate=" + validToDate +
                      ", owners=" + owners +
                      '}';
          }
      }
      ```

   1. `Vehicle.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.model;
      
      import com.fasterxml.jackson.annotation.JsonCreator;
      import com.fasterxml.jackson.annotation.JsonProperty;
      import software.amazon.qldb.tutorial.model.streams.RevisionData;
      
      /**
       * Represents a vehicle, serializable to (and from) Ion.
       */
      public final class Vehicle implements RevisionData {
          private final String vin;
          private final String type;
          private final int year;
          private final String make;
          private final String model;
          private final String color;
      
          @JsonCreator
          public Vehicle(@JsonProperty("VIN") final String vin,
                         @JsonProperty("Type") final String type,
                         @JsonProperty("Year") final int year,
                         @JsonProperty("Make") final String make,
                         @JsonProperty("Model") final String model,
                         @JsonProperty("Color") final String color) {
              this.vin = vin;
              this.type = type;
              this.year = year;
              this.make = make;
              this.model = model;
              this.color = color;
          }
      
          @JsonProperty("Color")
          public String getColor() {
              return color;
          }
      
          @JsonProperty("Make")
          public String getMake() {
              return make;
          }
      
          @JsonProperty("Model")
          public String getModel() {
              return model;
          }
      
          @JsonProperty("Type")
          public String getType() {
              return type;
          }
      
          @JsonProperty("VIN")
          public String getVin() {
              return vin;
          }
      
          @JsonProperty("Year")
          public int getYear() {
              return year;
          }
      
          @Override
          public String toString() {
              return "Vehicle{" +
                      "vin='" + vin + '\'' +
                      ", type='" + type + '\'' +
                      ", year=" + year +
                      ", make='" + make + '\'' +
                      ", model='" + model + '\'' +
                      ", color='" + color + '\'' +
                      '}';
          }
      }
      ```

   1. `Owner.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.model;
      
      import com.fasterxml.jackson.annotation.JsonProperty;
      
      /**
       * Represents a vehicle owner, serializable to (and from) Ion.
       */ 
      public final class Owner {
          private final String personId;
      
          public Owner(@JsonProperty("PersonId") final String personId) {
              this.personId = personId;
          }
      
          @JsonProperty("PersonId")
          public String getPersonId() {
              return personId;
          }
      
          @Override
          public String toString() {
              return "Owner{" +
                      "personId='" + personId + '\'' +
                      '}';
          }
      }
      ```

   1. `Owners.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.model;
      
      import com.fasterxml.jackson.annotation.JsonProperty;
      
      import java.util.List;
      
      /**
       * Represents a set of owners for a given vehicle, serializable to (and from) Ion.
       */ 
      public final class Owners {
          private final Owner primaryOwner;
          private final List<Owner> secondaryOwners;
      
          public Owners(@JsonProperty("PrimaryOwner") final Owner primaryOwner,
                        @JsonProperty("SecondaryOwners") final List<Owner> secondaryOwners) {
              this.primaryOwner = primaryOwner;
              this.secondaryOwners = secondaryOwners;
          }
      
          @JsonProperty("PrimaryOwner")
          public Owner getPrimaryOwner() {
              return primaryOwner;
          }
      
          @JsonProperty("SecondaryOwners")
          public List<Owner> getSecondaryOwners() {
              return secondaryOwners;
          }
      
          @Override
          public String toString() {
              return "Owners{" +
                      "primaryOwner=" + primaryOwner +
                      ", secondaryOwners=" + secondaryOwners +
                      '}';
          }
      }
      ```

   1. `DmlResultDocument.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.qldb;
      
      import com.fasterxml.jackson.annotation.JsonCreator;
      import com.fasterxml.jackson.annotation.JsonProperty;
      
      /**
       * Contains information about an individual document inserted or modified
       * as a result of DML.
       */
      public class DmlResultDocument {
      
          private String documentId;
      
          @JsonCreator
          public DmlResultDocument(@JsonProperty("documentId") final String documentId) {
              this.documentId = documentId;
          }
      
          public String getDocumentId() {
              return documentId;
          }
      
          @Override
          public String toString() {
              return "DmlResultDocument{"
                  + "documentId='" + documentId + '\''
                  + '}';
          }
      }
      ```

   1. `RevisionData.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.model.streams;
      
      /**
       * Allows modeling the content of all revisions as a generic revision data. Used
       * in the {@link Revision} and extended by domain models in {@link
       * software.amazon.qldb.tutorial.model} to make it easier to write the {@link
       * Revision.RevisionDataDeserializer} that must deserialize the {@link
       * Revision#data} from different domain models.
       */
      public interface RevisionData { }
      ```

   1. `RevisionMetadata.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.qldb;
      
      import com.amazon.ion.IonInt;
      import com.amazon.ion.IonString;
      import com.amazon.ion.IonStruct;
      import com.amazon.ion.IonTimestamp;
      import com.fasterxml.jackson.annotation.JsonCreator;
      import com.fasterxml.jackson.annotation.JsonProperty;
      import com.fasterxml.jackson.databind.annotation.JsonSerialize;
      import com.fasterxml.jackson.dataformat.ion.IonTimestampSerializers;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      
      import java.util.Date;
      import java.util.Objects;
      
      /**
       * Represents the metadata field of a QLDB Document
       */
      public class RevisionMetadata {
          private static final Logger log = LoggerFactory.getLogger(RevisionMetadata.class);
          private final String id;
          private final long version;
          @JsonSerialize(using = IonTimestampSerializers.IonTimestampJavaDateSerializer.class)
          private final Date txTime;
          private final String txId;
      
          @JsonCreator
          public RevisionMetadata(@JsonProperty("id") final String id,
                                  @JsonProperty("version") final long version,
                                  @JsonProperty("txTime") final Date txTime,
                                  @JsonProperty("txId") final String txId) {
              this.id = id;
              this.version = version;
              this.txTime = txTime;
              this.txId = txId;
          }
      
          /**
           * Gets the unique ID of a QLDB document.
           *
           * @return the document ID.
           */
          public String getId() {
              return id;
          }
      
          /**
           * Gets the version number of the document in the document's modification history.
           * @return the version number.
           */
          public long getVersion() {
              return version;
          }
      
          /**
           * Gets the time during which the document was modified.
           *
           * @return the transaction time.
           */
          public Date getTxTime() {
              return txTime;
          }
      
          /**
           * Gets the transaction ID associated with this document.
           *
           * @return the transaction ID.
           */
          public String getTxId() {
              return txId;
          }
      
          public static RevisionMetadata fromIon(final IonStruct ionStruct) {
              if (ionStruct == null) {
                  throw new IllegalArgumentException("Metadata cannot be null");
              }
              try {
                  IonString id = (IonString) ionStruct.get("id");
                  IonInt version = (IonInt) ionStruct.get("version");
                  IonTimestamp txTime = (IonTimestamp) ionStruct.get("txTime");
                  IonString txId = (IonString) ionStruct.get("txId");
                  if (id == null || version == null || txTime == null || txId == null) {
                      throw new IllegalArgumentException("Document is missing required fields");
                  }
                  return new RevisionMetadata(id.stringValue(), version.longValue(), new Date(txTime.getMillis()), txId.stringValue());
              } catch (ClassCastException e) {
                  log.error("Failed to parse ion document");
                  throw new IllegalArgumentException("Document members are not of the correct type", e);
              }
          }
      
          /**
           * Converts a {@link RevisionMetadata} object to a string.
           *
           * @return the string representation of the {@link QldbRevision} object.
           */
          @Override
          public String toString() {
              return "Metadata{"
                      + "id='" + id + '\''
                      + ", version=" + version
                      + ", txTime=" + txTime
                      + ", txId='" + txId
                      + '\''
                      + '}';
          }
      
          /**
           * Check whether two {@link RevisionMetadata} objects are equivalent.
           *
           * @return {@code true} if the two objects are equal, {@code false} otherwise.
           */
          @Override
          public boolean equals(Object o) {
              if (this == o) { return true; }
              if (o == null || getClass() != o.getClass()) { return false; }
              RevisionMetadata metadata = (RevisionMetadata) o;
              return version == metadata.version
                      && id.equals(metadata.id)
                      && txTime.equals(metadata.txTime)
                      && txId.equals(metadata.txId);
          }
      
          /**
           * Generate a hash code for the {@link RevisionMetadata} object.
           *
           * @return the hash code.
           */
          @Override
          public int hashCode() {
              // CHECKSTYLE:OFF - Disabling as we are generating a hashCode of multiple properties.
              return Objects.hash(id, version, txTime, txId);
              // CHECKSTYLE:ON
          }
      }
      ```

   1. `QldbRevision.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.qldb;
      
      import com.amazon.ion.IonBlob;
      import com.amazon.ion.IonStruct;
      import com.fasterxml.jackson.annotation.JsonCreator;
      import com.fasterxml.jackson.annotation.JsonProperty;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      import software.amazon.qldb.tutorial.Constants;
      import software.amazon.qldb.tutorial.Verifier;
      
      import java.io.IOException;
      import java.util.Arrays;
      import java.util.Objects;
      
      /**
       * Represents a QldbRevision including both user data and metadata.
       */
      public final class QldbRevision {
          private static final Logger log = LoggerFactory.getLogger(QldbRevision.class);
      
          private final BlockAddress blockAddress;
          private final RevisionMetadata metadata;
          private final byte[] hash;
          private final byte[] dataHash;
          private final IonStruct data;
      
          @JsonCreator
          public QldbRevision(@JsonProperty("blockAddress") final BlockAddress blockAddress,
                              @JsonProperty("metadata") final RevisionMetadata metadata,
                              @JsonProperty("hash") final byte[] hash,
                              @JsonProperty("dataHash") final byte[] dataHash,
                              @JsonProperty("data") final IonStruct data) {
              this.blockAddress = blockAddress;
              this.metadata = metadata;
              this.hash = hash;
              this.dataHash = dataHash;
              this.data = data;
          }
      
          /**
           * Gets the unique ID of a QLDB document.
           *
           * @return the {@link BlockAddress} object.
           */
          public BlockAddress getBlockAddress() {
              return blockAddress;
          }
      
          /**
           * Gets the metadata of the revision.
           *
           * @return the {@link RevisionMetadata} object.
           */
          public RevisionMetadata getMetadata() {
              return metadata;
          }
      
          /**
           * Gets the SHA-256 hash value of the revision.
           * This is equivalent to the hash of the revision metadata and data.
           *
           * @return the byte array representing the hash.
           */
          public byte[] getHash() {
              return hash;
          }
      
          /**
           * Gets the SHA-256 hash value of the data portion of the revision.
           * This is only present if the revision is redacted.
           *
           * @return the byte array representing the hash.
           */
          public byte[] getDataHash() {
              return dataHash;
          }
      
          /**
           * Gets the revision data.
           *
           * @return the revision data.
           */
          public IonStruct getData() {
              return data;
          }
      
          /**
           * Returns true if the revision has been redacted.
           * @return a boolean value representing the redaction status
           * of this revision.
           */
          public Boolean isRedacted() {
              return dataHash != null;
          }
      
          /**
           * Constructs a new {@link QldbRevision} from an {@link IonStruct}.
           *
           * The specified {@link IonStruct} must include the following fields
           *
           * - blockAddress -- a {@link BlockAddress},
           * - metadata -- a {@link RevisionMetadata},
           * - hash -- the revision's hash calculated by QLDB,
           * - dataHash -- the user data's hash calculated by QLDB (only present if revision is redacted),
           * - data -- an {@link IonStruct} containing user data in the document.
           *
           * If any of these fields are missing or are malformed, then throws {@link IllegalArgumentException}.
           *
           * If the document hash calculated from the members of the specified {@link IonStruct} does not match
           * the hash member of the {@link IonStruct} then throws {@link IllegalArgumentException}.
           *
           * @param ionStruct
           *              The {@link IonStruct} that contains a {@link QldbRevision} object.
           * @return the converted {@link QldbRevision} object.
           * @throws IOException if failed to parse parameter {@link IonStruct}.
           */
          public static QldbRevision fromIon(final IonStruct ionStruct) throws IOException {
              try {
                  BlockAddress blockAddress = Constants.MAPPER.readValue(ionStruct.get("blockAddress"), BlockAddress.class);
                  IonBlob revisionHash = (IonBlob) ionStruct.get("hash");
                  IonStruct metadataStruct = (IonStruct) ionStruct.get("metadata");
                  IonStruct data = ionStruct.get("data") == null || ionStruct.get("data").isNullValue() ?
                      null : (IonStruct) ionStruct.get("data");
                  IonBlob dataHash = ionStruct.get("dataHash") == null || ionStruct.get("dataHash").isNullValue() ?
                      null : (IonBlob) ionStruct.get("dataHash");
                  if (revisionHash == null || metadataStruct == null) {
                      throw new IllegalArgumentException("Document is missing required fields");
                  }
                  byte[] dataHashBytes = dataHash != null ? dataHash.getBytes() : QldbIonUtils.hashIonValue(data);
                  verifyRevisionHash(metadataStruct, dataHashBytes, revisionHash.getBytes());
                  RevisionMetadata metadata = RevisionMetadata.fromIon(metadataStruct);
                  return new QldbRevision(
                          blockAddress,
                          metadata,
                          revisionHash.getBytes(),
                          dataHash != null ? dataHash.getBytes() : null,
                          data
                  );
              } catch (ClassCastException e) {
                  log.error("Failed to parse ion document");
                  throw new IllegalArgumentException("Document members are not of the correct type", e);
              }
          }
      
          /**
           * Converts a {@link QldbRevision} object to string.
           *
           * @return the string representation of the {@link QldbRevision} object.
           */
          @Override
          public String toString() {
              return "QldbRevision{" +
                      "blockAddress=" + blockAddress +
                      ", metadata=" + metadata +
                      ", hash=" + Arrays.toString(hash) +
                      ", dataHash=" + Arrays.toString(dataHash) +
                      ", data=" + data +
                      '}';
          }
      
          /**
           * Check whether two {@link QldbRevision} objects are equivalent.
           *
           * @return {@code true} if the two objects are equal, {@code false} otherwise.
           */
          @Override
          public boolean equals(final Object o) {
              if (this == o) {
                  return true;
              }
              if (!(o instanceof QldbRevision)) {
                  return false;
              }
              final QldbRevision that = (QldbRevision) o;
              return Objects.equals(getBlockAddress(), that.getBlockAddress())
                      && Objects.equals(getMetadata(), that.getMetadata())
                      && Arrays.equals(getHash(), that.getHash())
                      && Arrays.equals(getDataHash(), that.getDataHash())
                      && Objects.equals(getData(), that.getData());
          }
      
          /**
           * Create a hash code for the {@link QldbRevision} object.
           *
           * @return the hash code.
           */
          @Override
          public int hashCode() {
              // CHECKSTYLE:OFF - Disabling as we are generating a hashCode of multiple properties.
              int result = Objects.hash(blockAddress, metadata, data);
              // CHECKSTYLE:ON
              result = 31 * result + Arrays.hashCode(hash);
              return result;
          }
      
          /**
           * Throws an IllegalArgumentException if the hash of the revision data and metadata
           * does not match the hash provided by QLDB with the revision.
           */
          public void verifyRevisionHash() {
              // Certain internal-only system revisions only contain a hash which cannot be
              // further computed. However, these system hashes still participate to validate
              // the journal block. User revisions will always contain values for all fields
              // and can therefore have their hash computed.
              if (blockAddress == null && metadata == null && data == null && dataHash == null) {
                  return;
              }
      
              try {
                  IonStruct metadataIon = (IonStruct) Constants.MAPPER.writeValueAsIonValue(metadata);
                  byte[] dataHashBytes = isRedacted() ? dataHash : QldbIonUtils.hashIonValue(data);
                  verifyRevisionHash(metadataIon, dataHashBytes, hash);
              } catch (IOException e) {
                  throw new IllegalArgumentException("Could not encode revision metadata to ion.", e);
              }
          }
      
          private static void verifyRevisionHash(IonStruct metadata, byte[] dataHash, byte[] expectedHash) {
              byte[] metadataHash = QldbIonUtils.hashIonValue(metadata);
              byte[] candidateHash = Verifier.dot(metadataHash, dataHash);
              if (!Arrays.equals(candidateHash, expectedHash)) {
                  throw new IllegalArgumentException("Hash entry of QLDB revision and computed hash "
                          + "of QLDB revision do not match");
              }
          }
      }
      ```

   1. `IonLocalDateDeserializer.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.model;
      
      import com.amazon.ion.Timestamp;
      import com.fasterxml.jackson.core.JsonParser;
      import com.fasterxml.jackson.databind.DeserializationContext;
      import com.fasterxml.jackson.databind.JsonDeserializer;
      
      import java.io.IOException;
      import java.time.LocalDate;
      
      /**
       * Deserializes [java.time.LocalDate] from Ion.
       */
      public class IonLocalDateDeserializer extends JsonDeserializer<LocalDate> {
      
          @Override
          public LocalDate deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
              return timestampToLocalDate((Timestamp) jp.getEmbeddedObject());
          }
      
          private LocalDate timestampToLocalDate(Timestamp timestamp) {
              return LocalDate.of(timestamp.getYear(), timestamp.getMonth(), timestamp.getDay());
          }
      }
      ```

   1. `IonLocalDateSerializer.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.model;
      
      import com.amazon.ion.Timestamp;
      import com.fasterxml.jackson.core.JsonGenerator;
      import com.fasterxml.jackson.databind.SerializerProvider;
      import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer;
      import com.fasterxml.jackson.dataformat.ion.IonGenerator;
      
      import java.io.IOException;
      import java.time.LocalDate;
      
      /**
       * Serializes [java.time.LocalDate] to Ion.
       */
      public class IonLocalDateSerializer extends StdScalarSerializer<LocalDate> {
      
          public IonLocalDateSerializer() {
              super(LocalDate.class);
          }
      
          @Override
          public void serialize(LocalDate date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
              Timestamp timestamp = Timestamp.forDay(date.getYear(), date.getMonthValue(), date.getDayOfMonth());
              ((IonGenerator) jsonGenerator).writeValue(timestamp);
          }
      }
      ```

1. 查看以下文件（`SampleData.java`），该文件代表您插入 `vehicle-registration` 表中的示例数据。

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial.model;
   
   import com.amazon.ion.IonString;
   import com.amazon.ion.IonStruct;
   import com.amazon.ion.IonValue;
   import java.io.IOException;
   import java.math.BigDecimal;
   import java.text.ParseException;
   import java.time.LocalDate;
   import java.time.format.DateTimeFormatter;
   import java.util.ArrayList;
   import java.util.Arrays;
   import java.util.Collections;
   import java.util.List;
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.ConnectToLedger;
   import software.amazon.qldb.tutorial.Constants;
   import software.amazon.qldb.tutorial.qldb.DmlResultDocument;
   import software.amazon.qldb.tutorial.qldb.QldbRevision;
   
   /**
    * Sample domain objects for use throughout this tutorial.
    */
   public final class SampleData {
       public static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
   
       public static final List<VehicleRegistration> REGISTRATIONS = Collections.unmodifiableList(Arrays.asList(
               new VehicleRegistration("1N4AL11D75C109151", "LEWISR261LL", "WA", "Seattle",
                       BigDecimal.valueOf(90.25), convertToLocalDate("2017-08-21"), convertToLocalDate("2020-05-11"),
                       new Owners(new Owner(null), Collections.emptyList())),
               new VehicleRegistration("KM8SRDHF6EU074761", "CA762X", "WA", "Kent",
                       BigDecimal.valueOf(130.75), convertToLocalDate("2017-09-14"), convertToLocalDate("2020-06-25"),
                       new Owners(new Owner(null), Collections.emptyList())),
               new VehicleRegistration("3HGGK5G53FM761765", "CD820Z", "WA", "Everett",
                       BigDecimal.valueOf(442.30), convertToLocalDate("2011-03-17"), convertToLocalDate("2021-03-24"),
                       new Owners(new Owner(null), Collections.emptyList())),
               new VehicleRegistration("1HVBBAANXWH544237", "LS477D", "WA", "Tacoma",
                       BigDecimal.valueOf(42.20), convertToLocalDate("2011-10-26"), convertToLocalDate("2023-09-25"),
                       new Owners(new Owner(null), Collections.emptyList())),
               new VehicleRegistration("1C4RJFAG0FC625797", "TH393F", "WA", "Olympia",
                       BigDecimal.valueOf(30.45), convertToLocalDate("2013-09-02"), convertToLocalDate("2024-03-19"),
                       new Owners(new Owner(null), Collections.emptyList()))
       ));
   
       public static final List<Vehicle> VEHICLES = Collections.unmodifiableList(Arrays.asList(
               new Vehicle("1N4AL11D75C109151", "Sedan", 2011,  "Audi", "A5", "Silver"),
               new Vehicle("KM8SRDHF6EU074761", "Sedan", 2015, "Tesla", "Model S", "Blue"),
               new Vehicle("3HGGK5G53FM761765", "Motorcycle", 2011, "Ducati", "Monster 1200", "Yellow"),
               new Vehicle("1HVBBAANXWH544237", "Semi", 2009, "Ford", "F 150", "Black"),
               new Vehicle("1C4RJFAG0FC625797", "Sedan", 2019, "Mercedes", "CLK 350", "White")
       ));
   
       public static final List<Person> PEOPLE = Collections.unmodifiableList(Arrays.asList(
               new Person("Raul", "Lewis", convertToLocalDate("1963-08-19"),
                       "LEWISR261LL", "Driver License",  "1719 University Street, Seattle, WA, 98109"),
               new Person("Brent", "Logan", convertToLocalDate("1967-07-03"),
                       "LOGANB486CG", "Driver License", "43 Stockert Hollow Road, Everett, WA, 98203"),
               new Person("Alexis", "Pena", convertToLocalDate("1974-02-10"),
                       "744 849 301", "SSN", "4058 Melrose Street, Spokane Valley, WA, 99206"),
               new Person("Melvin", "Parker", convertToLocalDate("1976-05-22"),
                       "P626-168-229-765", "Passport", "4362 Ryder Avenue, Seattle, WA, 98101"),
               new Person("Salvatore", "Spencer", convertToLocalDate("1997-11-15"),
                       "S152-780-97-415-0", "Passport", "4450 Honeysuckle Lane, Seattle, WA, 98101")
       ));
   
       public static final List<DriversLicense> LICENSES = Collections.unmodifiableList(Arrays.asList(
               new DriversLicense(null, "LEWISR261LL", "Learner",
                       convertToLocalDate("2016-12-20"), convertToLocalDate("2020-11-15")),
               new DriversLicense(null, "LOGANB486CG", "Probationary",
                       convertToLocalDate("2016-04-06"), convertToLocalDate("2020-11-15")),
               new DriversLicense(null, "744 849 301", "Full",
                       convertToLocalDate("2017-12-06"), convertToLocalDate("2022-10-15")),
               new DriversLicense(null, "P626-168-229-765", "Learner",
                       convertToLocalDate("2017-08-16"), convertToLocalDate("2021-11-15")),
               new DriversLicense(null, "S152-780-97-415-0", "Probationary",
                       convertToLocalDate("2015-08-15"), convertToLocalDate("2021-08-21"))
       ));
   
       private SampleData() { }
   
       /**
        * Converts a date string with the format 'yyyy-MM-dd' into a {@link java.util.Date} object.
        *
        * @param date
        *              The date string to convert.
        * @return {@link java.time.LocalDate} or null if there is a {@link ParseException}
        */
       public static synchronized LocalDate convertToLocalDate(String date) {
           return LocalDate.parse(date, DATE_TIME_FORMAT);
       }
   
       /**
        * Convert the result set into a list of IonValues.
        *
        * @param result
        *              The result set to convert.
        * @return a list of IonValues.
        */
       public static List<IonValue> toIonValues(Result result) {
           final List<IonValue> valueList = new ArrayList<>();
           result.iterator().forEachRemaining(valueList::add);
           return valueList;
       }
   
       /**
        * Get the document ID of a particular document.
        *
        * @param txn
        *              A transaction executor object.
        * @param tableName
        *              Name of the table containing the document.
        * @param identifier
        *              The identifier used to narrow down the search.
        * @param value
        *              Value of the identifier.
        * @return the list of document IDs in the result set.
        */
       public static String getDocumentId(final TransactionExecutor txn, final String tableName,
                                          final String identifier, final String value) {
           try {
               final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(value));
               final String query = String.format("SELECT metadata.id FROM _ql_committed_%s AS p WHERE p.data.%s = ?",
                       tableName, identifier);
               Result result = txn.execute(query, parameters);
               if (result.isEmpty()) {
                   throw new IllegalStateException("Unable to retrieve document ID using " + value);
               }
               return getStringValueOfStructField((IonStruct) result.iterator().next(), "id");
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Get the document by ID.
        *
        * @param tableName
        *              Name of the table to insert documents into.
        * @param documentId
        *              The unique ID of a document in the Person table.
        * @return a {@link QldbRevision} object.
        * @throws IllegalStateException if failed to convert parameter into {@link IonValue}.
        */
       public static QldbRevision getDocumentById(String tableName, String documentId) {
           try {
               final IonValue ionValue = Constants.MAPPER.writeValueAsIonValue(documentId);
               Result result = ConnectToLedger.getDriver().execute(txn -> {
                   return txn.execute("SELECT c.* FROM _ql_committed_" + tableName + " AS c BY docId "
                                      + "WHERE docId = ?", ionValue);
               });
               if (result.isEmpty()) {
                   throw new IllegalStateException("Unable to retrieve document by id " + documentId + " in table " + tableName);
               }
               return Constants.MAPPER.readValue(result.iterator().next(), QldbRevision.class);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Return a list of modified document IDs as strings from a DML {@link Result}.
        *
        * @param result
        *              The result set from a DML operation.
        * @return the list of document IDs modified by the operation.
        */
       public static List<String> getDocumentIdsFromDmlResult(final Result result) {
           final List<String> strings = new ArrayList<>();
           result.iterator().forEachRemaining(row -> strings.add(getDocumentIdFromDmlResultDocument(row)));
           return strings;
       }
   
       /**
        * Convert the given DML result row's document ID to string.
        *
        * @param dmlResultDocument
        *              The {@link IonValue} representing the results of a DML operation.
        * @return a string of document ID.
        */
       public static String getDocumentIdFromDmlResultDocument(final IonValue dmlResultDocument) {
           try {
               DmlResultDocument result = Constants.MAPPER.readValue(dmlResultDocument, DmlResultDocument.class);
               return result.getDocumentId();
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Get the String value of a given {@link IonStruct} field name.
        * @param struct the {@link IonStruct} from which to get the value.
        * @param fieldName the name of the field from which to get the value.
        * @return the String value of the field within the given {@link IonStruct}.
        */
       public static String getStringValueOfStructField(final IonStruct struct, final String fieldName) {
           return ((IonString) struct.get(fieldName)).stringValue();
       }
   
       /**
        * Return a copy of the given driver's license with updated person Id.
        *
        * @param oldLicense
        *              The old driver's license to update.
        * @param personId
        *              The PersonId of the driver.
        * @return the updated {@link DriversLicense}.
        */
       public static DriversLicense updatePersonIdDriversLicense(final DriversLicense oldLicense, final String personId) {
           return new DriversLicense(personId, oldLicense.getLicenseNumber(), oldLicense.getLicenseType(),
                   oldLicense.getValidFromDate(), oldLicense.getValidToDate());
       }
   
       /**
        * Return a copy of the given vehicle registration with updated person Id.
        *
        * @param oldRegistration
        *              The old vehicle registration to update.
        * @param personId
        *              The PersonId of the driver.
        * @return the updated {@link VehicleRegistration}.
        */
       public static VehicleRegistration updateOwnerVehicleRegistration(final VehicleRegistration oldRegistration,
                                                                        final String personId) {
           return new VehicleRegistration(oldRegistration.getVin(), oldRegistration.getLicensePlateNumber(),
                   oldRegistration.getState(), oldRegistration.getCity(), oldRegistration.getPendingPenaltyTicketAmount(),
                   oldRegistration.getValidFromDate(), oldRegistration.getValidToDate(),
                   new Owners(new Owner(personId), Collections.emptyList()));
       }
   }
   ```

------
#### [ 1.x ]

**重要**  
对于 Amazon Ion 软件包，您必须在应用程序中使用命名空间`com.amazon.ion`。 适用于 Java 的 AWS SDK 依赖于命名空间下的另一个 Ion 包`software.amazon.ion`，但这是一个与 QLDB 驱动程序不兼容的旧包。

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial.model;
   
   import com.amazon.ion.IonString;
   import com.amazon.ion.IonStruct;
   import com.amazon.ion.IonValue;
   import software.amazon.qldb.QldbSession;
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.Constants;
   import software.amazon.qldb.tutorial.qldb.DmlResultDocument;
   import software.amazon.qldb.tutorial.qldb.QldbRevision;
   
   import java.io.IOException;
   
   import java.math.BigDecimal;
   import java.text.ParseException;
   import java.time.LocalDate;
   import java.time.format.DateTimeFormatter;
   import java.util.ArrayList;
   import java.util.Arrays;
   import java.util.Collections;
   import java.util.List;
   
   /**
    * Sample domain objects for use throughout this tutorial.
    */
   public final class SampleData {
       public static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
   
       public static final List<VehicleRegistration> REGISTRATIONS = Collections.unmodifiableList(Arrays.asList(
               new VehicleRegistration("1N4AL11D75C109151", "LEWISR261LL", "WA", "Seattle",
                       BigDecimal.valueOf(90.25), convertToLocalDate("2017-08-21"), convertToLocalDate("2020-05-11"),
                       new Owners(new Owner(null), Collections.emptyList())),
               new VehicleRegistration("KM8SRDHF6EU074761", "CA762X", "WA", "Kent",
                       BigDecimal.valueOf(130.75), convertToLocalDate("2017-09-14"), convertToLocalDate("2020-06-25"),
                       new Owners(new Owner(null), Collections.emptyList())),
               new VehicleRegistration("3HGGK5G53FM761765", "CD820Z", "WA", "Everett",
                       BigDecimal.valueOf(442.30), convertToLocalDate("2011-03-17"), convertToLocalDate("2021-03-24"),
                       new Owners(new Owner(null), Collections.emptyList())),
               new VehicleRegistration("1HVBBAANXWH544237", "LS477D", "WA", "Tacoma",
                       BigDecimal.valueOf(42.20), convertToLocalDate("2011-10-26"), convertToLocalDate("2023-09-25"),
                       new Owners(new Owner(null), Collections.emptyList())),
               new VehicleRegistration("1C4RJFAG0FC625797", "TH393F", "WA", "Olympia",
                       BigDecimal.valueOf(30.45), convertToLocalDate("2013-09-02"), convertToLocalDate("2024-03-19"),
                       new Owners(new Owner(null), Collections.emptyList()))
       ));
   
       public static final List<Vehicle> VEHICLES = Collections.unmodifiableList(Arrays.asList(
               new Vehicle("1N4AL11D75C109151", "Sedan", 2011,  "Audi", "A5", "Silver"),
               new Vehicle("KM8SRDHF6EU074761", "Sedan", 2015, "Tesla", "Model S", "Blue"),
               new Vehicle("3HGGK5G53FM761765", "Motorcycle", 2011, "Ducati", "Monster 1200", "Yellow"),
               new Vehicle("1HVBBAANXWH544237", "Semi", 2009, "Ford", "F 150", "Black"),
               new Vehicle("1C4RJFAG0FC625797", "Sedan", 2019, "Mercedes", "CLK 350", "White")
       ));
   
       public static final List<Person> PEOPLE = Collections.unmodifiableList(Arrays.asList(
               new Person("Raul", "Lewis", convertToLocalDate("1963-08-19"),
                       "LEWISR261LL", "Driver License",  "1719 University Street, Seattle, WA, 98109"),
               new Person("Brent", "Logan", convertToLocalDate("1967-07-03"),
                       "LOGANB486CG", "Driver License", "43 Stockert Hollow Road, Everett, WA, 98203"),
               new Person("Alexis", "Pena", convertToLocalDate("1974-02-10"),
                       "744 849 301", "SSN", "4058 Melrose Street, Spokane Valley, WA, 99206"),
               new Person("Melvin", "Parker", convertToLocalDate("1976-05-22"),
                       "P626-168-229-765", "Passport", "4362 Ryder Avenue, Seattle, WA, 98101"),
               new Person("Salvatore", "Spencer", convertToLocalDate("1997-11-15"),
                       "S152-780-97-415-0", "Passport", "4450 Honeysuckle Lane, Seattle, WA, 98101")
       ));
   
       public static final List<DriversLicense> LICENSES = Collections.unmodifiableList(Arrays.asList(
               new DriversLicense(null, "LEWISR261LL", "Learner",
                       convertToLocalDate("2016-12-20"), convertToLocalDate("2020-11-15")),
               new DriversLicense(null, "LOGANB486CG", "Probationary",
                       convertToLocalDate("2016-04-06"), convertToLocalDate("2020-11-15")),
               new DriversLicense(null, "744 849 301", "Full",
                       convertToLocalDate("2017-12-06"), convertToLocalDate("2022-10-15")),
               new DriversLicense(null, "P626-168-229-765", "Learner",
                       convertToLocalDate("2017-08-16"), convertToLocalDate("2021-11-15")),
               new DriversLicense(null, "S152-780-97-415-0", "Probationary",
                       convertToLocalDate("2015-08-15"), convertToLocalDate("2021-08-21"))
       ));
   
       private SampleData() { }
   
       /**
        * Converts a date string with the format 'yyyy-MM-dd' into a {@link java.util.Date} object.
        *
        * @param date
        *              The date string to convert.
        * @return {@link LocalDate} or null if there is a {@link ParseException}
        */
       public static synchronized LocalDate convertToLocalDate(String date) {
           return LocalDate.parse(date, DATE_TIME_FORMAT);
       }
   
       /**
        * Convert the result set into a list of IonValues.
        *
        * @param result
        *              The result set to convert.
        * @return a list of IonValues.
        */
       public static List<IonValue> toIonValues(Result result) {
           final List<IonValue> valueList = new ArrayList<>();
           result.iterator().forEachRemaining(valueList::add);
           return valueList;
       }
   
       /**
        * Get the document ID of a particular document.
        *
        * @param txn
        *              A transaction executor object.
        * @param tableName
        *              Name of the table containing the document.
        * @param identifier
        *              The identifier used to narrow down the search.
        * @param value
        *              Value of the identifier.
        * @return the list of document IDs in the result set.
        */
       public static String getDocumentId(final TransactionExecutor txn, final String tableName,
                                          final String identifier, final String value) {
           try {
               final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(value));
               final String query = String.format("SELECT metadata.id FROM _ql_committed_%s AS p WHERE p.data.%s = ?",
                       tableName, identifier);
               Result result = txn.execute(query, parameters);
               if (result.isEmpty()) {
                   throw new IllegalStateException("Unable to retrieve document ID using " + value);
               }
               return getStringValueOfStructField((IonStruct) result.iterator().next(), "id");
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Get the document by ID.
        *
        * @param qldbSession
        *              A QLDB session.
        * @param tableName
        *              Name of the table to insert documents into.
        * @param documentId
        *              The unique ID of a document in the Person table.
        * @return a {@link QldbRevision} object.
        * @throws IllegalStateException if failed to convert parameter into {@link IonValue}.
        */
       public static QldbRevision getDocumentById(QldbSession qldbSession, String tableName, String documentId) {
           try {
               final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(documentId));
               final String query = String.format("SELECT c.* FROM _ql_committed_%s AS c BY docId WHERE docId = ?", tableName);
               Result result = qldbSession.execute(query, parameters);
               if (result.isEmpty()) {
                   throw new IllegalStateException("Unable to retrieve document by id " + documentId + " in table " + tableName);
               }
               return Constants.MAPPER.readValue(result.iterator().next(), QldbRevision.class);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Return a list of modified document IDs as strings from a DML {@link Result}.
        *
        * @param result
        *              The result set from a DML operation.
        * @return the list of document IDs modified by the operation.
        */
       public static List<String> getDocumentIdsFromDmlResult(final Result result) {
           final List<String> strings = new ArrayList<>();
           result.iterator().forEachRemaining(row -> strings.add(getDocumentIdFromDmlResultDocument(row)));
           return strings;
       }
   
       /**
        * Convert the given DML result row's document ID to string.
        *
        * @param dmlResultDocument
        *              The {@link IonValue} representing the results of a DML operation.
        * @return a string of document ID.
        */
       public static String getDocumentIdFromDmlResultDocument(final IonValue dmlResultDocument) {
           try {
               DmlResultDocument result = Constants.MAPPER.readValue(dmlResultDocument, DmlResultDocument.class);
               return result.getDocumentId();
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Get the String value of a given {@link IonStruct} field name.
        * @param struct the {@link IonStruct} from which to get the value.
        * @param fieldName the name of the field from which to get the value.
        * @return the String value of the field within the given {@link IonStruct}.
        */
       public static String getStringValueOfStructField(final IonStruct struct, final String fieldName) {
           return ((IonString) struct.get(fieldName)).stringValue();
       }
   
       /**
        * Return a copy of the given driver's license with updated person Id.
        *
        * @param oldLicense
        *              The old driver's license to update.
        * @param personId
        *              The PersonId of the driver.
        * @return the updated {@link DriversLicense}.
        */
       public static DriversLicense updatePersonIdDriversLicense(final DriversLicense oldLicense, final String personId) {
           return new DriversLicense(personId, oldLicense.getLicenseNumber(), oldLicense.getLicenseType(),
                   oldLicense.getValidFromDate(), oldLicense.getValidToDate());
       }
   
       /**
        * Return a copy of the given vehicle registration with updated person Id.
        *
        * @param oldRegistration
        *              The old vehicle registration to update.
        * @param personId
        *              The PersonId of the driver.
        * @return the updated {@link VehicleRegistration}.
        */
       public static VehicleRegistration updateOwnerVehicleRegistration(final VehicleRegistration oldRegistration,
                                                                        final String personId) {
           return new VehicleRegistration(oldRegistration.getVin(), oldRegistration.getLicensePlateNumber(),
                   oldRegistration.getState(), oldRegistration.getCity(), oldRegistration.getPendingPenaltyTicketAmount(),
                   oldRegistration.getValidFromDate(), oldRegistration.getValidToDate(),
                   new Owners(new Owner(personId), Collections.emptyList()));
       }
   }
   ```

------
**注意**  
该类使用 Ion 库提供辅助方法，用于将您的数据与 Ion 格式进行相互转换。
该 `getDocumentId`方法对带有前缀`_ql_committed_`的表运行查询。这是保留的前缀，表示您要查询表的*已提交视图*。在此视图中，您的数据嵌套至`data`字段中，元数据嵌套至`metadata`字段中。

1. 编译并运行以下程序（`CreateTable.java`）以创建前面提到的表。

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.SampleData;
   
   /**
    * Create tables in a QLDB ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class CreateTable {
       public static final Logger log = LoggerFactory.getLogger(CreateTable.class);
   
       private CreateTable() { }
   
       /**
        * Registrations, vehicles, owners, and licenses tables being created in a single transaction.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param tableName
        *              Name of the table to be created.
        * @return the number of tables created.
        */
       public static int createTable(final TransactionExecutor txn, final String tableName) {
           log.info("Creating the '{}' table...", tableName);
           final String createTable = String.format("CREATE TABLE %s", tableName);
           final Result result = txn.execute(createTable);
           log.info("{} table created successfully.", tableName);
           return SampleData.toIonValues(result).size();
       }
   
       public static void main(final String... args) {
           ConnectToLedger.getDriver().execute(txn -> {
               createTable(txn, Constants.DRIVERS_LICENSE_TABLE_NAME);
               createTable(txn, Constants.PERSON_TABLE_NAME);
               createTable(txn, Constants.VEHICLE_TABLE_NAME);
               createTable(txn, Constants.VEHICLE_REGISTRATION_TABLE_NAME);
           });
       }
   }
   ```

------
#### [ 1.x ]

   ```
   /*
    * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.SampleData;
   
   /**
    * Create tables in a QLDB ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class CreateTable {
       public static final Logger log = LoggerFactory.getLogger(CreateTable.class);
   
       private CreateTable() { }
   
       /**
        * Registrations, vehicles, owners, and licenses tables being created in a single transaction.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param tableName
        *              Name of the table to be created.
        * @return the number of tables created.
        */
       public static int createTable(final TransactionExecutor txn, final String tableName) {
           log.info("Creating the '{}' table...", tableName);
           final String createTable = String.format("CREATE TABLE %s", tableName);
           final Result result = txn.execute(createTable);
           log.info("{} table created successfully.", tableName);
           return SampleData.toIonValues(result).size();
       }
   
       public static void main(final String... args) {
           ConnectToLedger.getDriver().execute(txn -> {
               createTable(txn, Constants.DRIVERS_LICENSE_TABLE_NAME);
               createTable(txn, Constants.PERSON_TABLE_NAME);
               createTable(txn, Constants.VEHICLE_TABLE_NAME);
               createTable(txn, Constants.VEHICLE_REGISTRATION_TABLE_NAME);
           }, (retryAttempt) -> log.info("Retrying due to OCC conflict..."));
       }
   }
   ```

------
**注意**  
该程序演示了如何将 `TransactionExecutor` lambda 传递至`execute`方法。在此示例中，您使用 lambda 表达式在单个事务中运行多个`CREATE TABLE` PartiQL 语句。  
该 `execute` 方法采用隐式启动事务，运行 lambda 中的所有语句，然后自动提交事务。

1. 如前所述，编译并运行以下程序（`CreateIndex.java`），以在表上创建索引。

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.SampleData;
   
   /**
    * Create indexes on tables in a particular ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class CreateIndex {
       public static final Logger log = LoggerFactory.getLogger(CreateIndex.class);
   
       private CreateIndex() { }
   
       /**
        * In this example, create indexes for registrations and vehicles tables.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param tableName
        *              Name of the table to be created.
        * @param indexAttribute
        *              The index attribute to use.
        * @return the number of tables created.
        */
       public static int createIndex(final TransactionExecutor txn, final String tableName, final String indexAttribute) {
           log.info("Creating an index on {}...", indexAttribute);
           final String createIndex = String.format("CREATE INDEX ON %s (%s)", tableName, indexAttribute);
           final Result r = txn.execute(createIndex);
           return SampleData.toIonValues(r).size();
       }
   
       public static void main(final String... args) {
           ConnectToLedger.getDriver().execute(txn -> {
               createIndex(txn, Constants.PERSON_TABLE_NAME, Constants.PERSON_GOV_ID_INDEX_NAME);
               createIndex(txn, Constants.VEHICLE_TABLE_NAME, Constants.VIN_INDEX_NAME);
               createIndex(txn, Constants.DRIVERS_LICENSE_TABLE_NAME, Constants.DRIVER_LICENSE_NUMBER_INDEX_NAME);
               createIndex(txn, Constants.DRIVERS_LICENSE_TABLE_NAME, Constants.DRIVER_LICENSE_PERSONID_INDEX_NAME);
               createIndex(txn, Constants.VEHICLE_REGISTRATION_TABLE_NAME, Constants.VIN_INDEX_NAME);
               createIndex(txn, Constants.VEHICLE_REGISTRATION_TABLE_NAME,
                       Constants.VEHICLE_REGISTRATION_LICENSE_PLATE_NUMBER_INDEX_NAME);
           });
           log.info("Indexes created successfully!");
       }
   }
   ```

------
#### [ 1.x ]

   ```
   /*
    * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.SampleData;
   
   /**
    * Create indexes on tables in a particular ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class CreateIndex {
       public static final Logger log = LoggerFactory.getLogger(CreateIndex.class);
   
       private CreateIndex() { }
   
       /**
        * In this example, create indexes for registrations and vehicles tables.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param tableName
        *              Name of the table to be created.
        * @param indexAttribute
        *              The index attribute to use.
        * @return the number of tables created.
        */
       public static int createIndex(final TransactionExecutor txn, final String tableName, final String indexAttribute) {
           log.info("Creating an index on {}...", indexAttribute);
           final String createIndex = String.format("CREATE INDEX ON %s (%s)", tableName, indexAttribute);
           final Result r = txn.execute(createIndex);
           return SampleData.toIonValues(r).size();
       }
   
       public static void main(final String... args) {
           ConnectToLedger.getDriver().execute(txn -> {
               createIndex(txn, Constants.PERSON_TABLE_NAME, Constants.PERSON_GOV_ID_INDEX_NAME);
               createIndex(txn, Constants.VEHICLE_TABLE_NAME, Constants.VIN_INDEX_NAME);
               createIndex(txn, Constants.DRIVERS_LICENSE_TABLE_NAME, Constants.DRIVER_LICENSE_NUMBER_INDEX_NAME);
               createIndex(txn, Constants.DRIVERS_LICENSE_TABLE_NAME, Constants.DRIVER_LICENSE_PERSONID_INDEX_NAME);
               createIndex(txn, Constants.VEHICLE_REGISTRATION_TABLE_NAME, Constants.VIN_INDEX_NAME);
               createIndex(txn, Constants.VEHICLE_REGISTRATION_TABLE_NAME,
                       Constants.VEHICLE_REGISTRATION_LICENSE_PLATE_NUMBER_INDEX_NAME);
           }, (retryAttempt) -> log.info("Retrying due to OCC conflict..."));
           log.info("Indexes created successfully!");
       }
   }
   ```

------

1. 编译并运行以下程序（`InsertDocument.java`），将示例数据插入表内。

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import java.io.IOException;
   import java.util.ArrayList;
   import java.util.Collections;
   import java.util.List;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import com.amazon.ion.IonValue;
   
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.DriversLicense;
   import software.amazon.qldb.tutorial.model.SampleData;
   import software.amazon.qldb.tutorial.model.VehicleRegistration;
   
   /**
    * Insert documents into a table in a QLDB ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class InsertDocument {
       public static final Logger log = LoggerFactory.getLogger(InsertDocument.class);
   
       private InsertDocument() { }
   
       /**
        * Insert the given list of documents into the specified table and return the document IDs of the inserted documents.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param tableName
        *              Name of the table to insert documents into.
        * @param documents
        *              List of documents to insert into the specified table.
        * @return a list of document IDs.
        * @throws IllegalStateException if failed to convert documents into an {@link IonValue}.
        */
       public static List<String> insertDocuments(final TransactionExecutor txn, final String tableName,
                                                  final List documents) {
           log.info("Inserting some documents in the {} table...", tableName);
           try {
               final String query = String.format("INSERT INTO %s ?", tableName);
               final IonValue ionDocuments = Constants.MAPPER.writeValueAsIonValue(documents);
   
               return SampleData.getDocumentIdsFromDmlResult(txn.execute(query, ionDocuments));
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Update PersonIds in driver's licenses and in vehicle registrations using document IDs.
        *
        * @param documentIds
        *              List of document IDs representing the PersonIds in DriversLicense and PrimaryOwners in VehicleRegistration.
        * @param licenses
        *              List of driver's licenses to update.
        * @param registrations
        *              List of registrations to update.
        */
       public static void updatePersonId(final List<String> documentIds, final List<DriversLicense> licenses,
                                         final List<VehicleRegistration> registrations) {
           for (int i = 0; i < documentIds.size(); ++i) {
               DriversLicense license = SampleData.LICENSES.get(i);
               VehicleRegistration registration = SampleData.REGISTRATIONS.get(i);
               licenses.add(SampleData.updatePersonIdDriversLicense(license, documentIds.get(i)));
               registrations.add(SampleData.updateOwnerVehicleRegistration(registration, documentIds.get(i)));
           }
       }
   
       public static void main(final String... args) {
           final List<DriversLicense> newDriversLicenses = new ArrayList<>();
           final List<VehicleRegistration> newVehicleRegistrations = new ArrayList<>();
           ConnectToLedger.getDriver().execute(txn -> {
               List<String> documentIds = insertDocuments(txn, Constants.PERSON_TABLE_NAME, SampleData.PEOPLE);
               updatePersonId(documentIds, newDriversLicenses, newVehicleRegistrations);
               insertDocuments(txn, Constants.VEHICLE_TABLE_NAME, SampleData.VEHICLES);
               insertDocuments(txn, Constants.VEHICLE_REGISTRATION_TABLE_NAME,
                       Collections.unmodifiableList(newVehicleRegistrations));
               insertDocuments(txn, Constants.DRIVERS_LICENSE_TABLE_NAME,
                       Collections.unmodifiableList(newDriversLicenses));
           });
           log.info("Documents inserted successfully!");
       }
   }
   ```

------
#### [ 1.x ]

   ```
   /*
    * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazon.ion.IonValue;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   import software.amazon.qldb.QldbSession;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.DriversLicense;
   import software.amazon.qldb.tutorial.model.SampleData;
   import software.amazon.qldb.tutorial.model.VehicleRegistration;
   
   import java.io.IOException;
   import java.util.ArrayList;
   import java.util.Collections;
   import java.util.List;
   
   /**
    * Insert documents into a table in a QLDB ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class InsertDocument {
       public static final Logger log = LoggerFactory.getLogger(InsertDocument.class);
   
       private InsertDocument() { }
   
       /**
        * Insert the given list of documents into the specified table and return the document IDs of the inserted documents.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param tableName
        *              Name of the table to insert documents into.
        * @param documents
        *              List of documents to insert into the specified table.
        * @return a list of document IDs.
        * @throws IllegalStateException if failed to convert documents into an {@link IonValue}.
        */
       public static List<String> insertDocuments(final TransactionExecutor txn, final String tableName,
                                                  final List documents) {
           log.info("Inserting some documents in the {} table...", tableName);
           try {
               final String statement = String.format("INSERT INTO %s ?", tableName);
               final IonValue ionDocuments = Constants.MAPPER.writeValueAsIonValue(documents);
               final List<IonValue> parameters = Collections.singletonList(ionDocuments);
               return SampleData.getDocumentIdsFromDmlResult(txn.execute(statement, parameters));
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Update PersonIds in driver's licenses and in vehicle registrations using document IDs.
        *
        * @param documentIds
        *              List of document IDs representing the PersonIds in DriversLicense and PrimaryOwners in VehicleRegistration.
        * @param licenses
        *              List of driver's licenses to update.
        * @param registrations
        *              List of registrations to update.
        */
       public static void updatePersonId(final List<String> documentIds, final List<DriversLicense> licenses,
                                         final List<VehicleRegistration> registrations) {
           for (int i = 0; i < documentIds.size(); ++i) {
               DriversLicense license = SampleData.LICENSES.get(i);
               VehicleRegistration registration = SampleData.REGISTRATIONS.get(i);
               licenses.add(SampleData.updatePersonIdDriversLicense(license, documentIds.get(i)));
               registrations.add(SampleData.updateOwnerVehicleRegistration(registration, documentIds.get(i)));
           }
       }
   
       public static void main(final String... args) {
           final List<DriversLicense> newDriversLicenses = new ArrayList<>();
           final List<VehicleRegistration> newVehicleRegistrations = new ArrayList<>();
           ConnectToLedger.getDriver().execute(txn -> {
               List<String> documentIds = insertDocuments(txn, Constants.PERSON_TABLE_NAME, SampleData.PEOPLE);
               updatePersonId(documentIds, newDriversLicenses, newVehicleRegistrations);
               insertDocuments(txn, Constants.VEHICLE_TABLE_NAME, SampleData.VEHICLES);
               insertDocuments(txn, Constants.VEHICLE_REGISTRATION_TABLE_NAME,
                       Collections.unmodifiableList(newVehicleRegistrations));
               insertDocuments(txn, Constants.DRIVERS_LICENSE_TABLE_NAME,
                       Collections.unmodifiableList(newDriversLicenses));
           }, (retryAttempt) -> log.info("Retrying due to OCC conflict..."));
           log.info("Documents inserted successfully!");
       }
   }
   ```

------
**注意**  
该程序演示如何使用参数化值调用`execute`方法。除了要运行的 PartiQL 语句之外，您还可以传递`IonValue`类数据参数。在语句字符串中将问号（`?`）作为变量占位符。
如果 `INSERT` 语句成功，则返回每个插入文档的`id`。

接下来，您可以使用 `SELECT` 语句从 `vehicle-registration` 分类账中的表中读取数据。继续执行[步骤 4：查询分类账中的表](getting-started.java.step-4.md)。

# 步骤 4：查询分类账中的表
<a name="getting-started.java.step-4"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在 Amazon QLDB 分类账中创建表格和加载数据后，您可运行查询以查看刚刚插入的车辆登记数据。QLDB 使用 [PartiQL](ql-reference.md)作为其查询语言，使用 [Amazon Ion](ion.md)作为其面向文档的数据模型。

PartiQL 是开源、与 SQL 兼容的查询语言，现已扩展为可与 Ion 配合使用。使用 PartiQL，您可使用熟悉的 SQL 运算符插入、查询和管理数据。Amazon Ion 是 JSON 的超集。Ion 是基于文档的开源数据格式，可让您灵活地存储和处理结构化、半结构化和嵌套数据。

在此步骤中，您将使用 `SELECT` 语句从 `vehicle-registration`分类账中的表中读取数据。

**警告**  
当您在没有索引查找的情况下运行查询时，它会调用全表扫描。PartiQL 之所以支持此类查询，是因为其与 SQL 兼容。但是，*切勿*在 QLDB 中对生产用例运行表扫描。表扫描可能会导致大型表出现性能问题，包括并发冲突与事务超时。  
为避免表扫描，必须在索引字段或文档 ID 上使用*相等*运算符（`WHERE indexedField = 123`或`WHERE indexedField IN (456, 789)`）运行带有`WHERE`谓词子句的语句。有关更多信息，请参阅 [优化查询性能](working.optimize.md)。

**查询表格**
+ 编译并运行以下程序（`FindVehicles.java`），以查询分类账中某人名下注册的所有车辆。

------
#### [ 2.x ]

  ```
  /*
   * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   * SPDX-License-Identifier: MIT-0
   *
   * Permission is hereby granted, free of charge, to any person obtaining a copy of this
   * software and associated documentation files (the "Software"), to deal in the Software
   * without restriction, including without limitation the rights to use, copy, modify,
   * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   * permit persons to whom the Software is furnished to do so.
   *
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   */
  
  package software.amazon.qldb.tutorial;
  
  import java.io.IOException;
  
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import com.amazon.ion.IonValue;
  
  import software.amazon.qldb.Result;
  import software.amazon.qldb.TransactionExecutor;
  import software.amazon.qldb.tutorial.model.Person;
  import software.amazon.qldb.tutorial.model.SampleData;
  
  /**
   * Find all vehicles registered under a person.
   *
   * This code expects that you have AWS credentials setup per:
   * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
   */
  public final class FindVehicles {
      public static final Logger log = LoggerFactory.getLogger(FindVehicles.class);
  
      private FindVehicles() { }
  
      /**
       * Find vehicles registered under a driver using their government ID.
       *
       * @param txn
       *              The {@link TransactionExecutor} for lambda execute.
       * @param govId
       *              The government ID of the owner.
       * @throws IllegalStateException if failed to convert parameters into {@link IonValue}.
       */
      public static void findVehiclesForOwner(final TransactionExecutor txn, final String govId) {
          try {
              final String documentId = Person.getDocumentIdByGovId(txn, govId);
              final String query = "SELECT v FROM Vehicle AS v INNER JOIN VehicleRegistration AS r "
                      + "ON v.VIN = r.VIN WHERE r.Owners.PrimaryOwner.PersonId = ?";
  
              final Result result = txn.execute(query, Constants.MAPPER.writeValueAsIonValue(documentId));
              log.info("List of Vehicles for owner with GovId: {}...", govId);
              ScanTable.printDocuments(result);
          } catch (IOException ioe) {
              throw new IllegalStateException(ioe);
          }
      }
  
      public static void main(final String... args) {
          final Person person = SampleData.PEOPLE.get(0);
          ConnectToLedger.getDriver().execute(txn -> {
              findVehiclesForOwner(txn, person.getGovId());
          });
      }
  }
  ```

------
#### [ 1.x ]

  ```
  /*
   * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
   * SPDX-License-Identifier: MIT-0
   *
   * Permission is hereby granted, free of charge, to any person obtaining a copy of this
   * software and associated documentation files (the "Software"), to deal in the Software
   * without restriction, including without limitation the rights to use, copy, modify,
   * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   * permit persons to whom the Software is furnished to do so.
   *
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   */
  
  package software.amazon.qldb.tutorial;
  
  import java.io.IOException;
  
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import com.amazon.ion.IonValue;
  
  import software.amazon.qldb.Result;
  import software.amazon.qldb.TransactionExecutor;
  import software.amazon.qldb.tutorial.model.Person;
  import software.amazon.qldb.tutorial.model.SampleData;
  
  /**
   * Find all vehicles registered under a person.
   *
   * This code expects that you have AWS credentials setup per:
   * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
   */
  public final class FindVehicles {
      public static final Logger log = LoggerFactory.getLogger(FindVehicles.class);
  
      private FindVehicles() { }
  
      /**
       * Find vehicles registered under a driver using their government ID.
       *
       * @param txn
       *              The {@link TransactionExecutor} for lambda execute.
       * @param govId
       *              The government ID of the owner.
       * @throws IllegalStateException if failed to convert parameters into {@link IonValue}.
       */
      public static void findVehiclesForOwner(final TransactionExecutor txn, final String govId) {
          try {
              final String documentId = Person.getDocumentIdByGovId(txn, govId);
              final String query = "SELECT v FROM Vehicle AS v INNER JOIN VehicleRegistration AS r "
                      + "ON v.VIN = r.VIN WHERE r.Owners.PrimaryOwner.PersonId = ?";
  
              final Result result = txn.execute(query, Constants.MAPPER.writeValueAsIonValue(documentId));
              log.info("List of Vehicles for owner with GovId: {}...", govId);
              ScanTable.printDocuments(result);
          } catch (IOException ioe) {
              throw new IllegalStateException(ioe);
          }
      }
  
      public static void main(final String... args) {
          final Person person = SampleData.PEOPLE.get(0);
          ConnectToLedger.getDriver().execute(txn -> {
              findVehiclesForOwner(txn, person.getGovId());
          }, (retryAttempt) -> log.info("Retrying due to OCC conflict..."));
      }
  }
  ```

------
**注意**  
首先，该程序在 `Person` 表格中查询文档`GovId LEWISR261LL`，以获取其`id`元数据字段。  
然后，它使用文档`id`，通过`PrimaryOwner.PersonId`作为外键查询`VehicleRegistration`表。它还结合`VIN`字段上的`Vehicle`表`VehicleRegistration`。

要了解如何修改 `vehicle-registration` 分类账表格中的文档，请参阅 [第 5 步：修改分类账中的文档](getting-started.java.step-5.md)。

# 第 5 步：修改分类账中的文档
<a name="getting-started.java.step-5"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

现在您有了要处理的数据，可以开始在 Amazon QLDB 中对 `vehicle-registration` 分类账文档进行更改。在此步骤中，以下代码示例介绍了如何运行数据操作语言（DML）语句。这些声明更新一辆车的主要车主，并为另一辆车添加了次要车主。

**修改文档**

1. 编译并运行以下程序（`TransferVehicleOwnership.java`），使用分类账中的 VIN`1N4AL11D75C109151` 更新车辆的主要车主。

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazon.ion.IonReader;
   import com.amazon.ion.IonStruct;
   import com.amazon.ion.IonValue;
   import com.amazon.ion.system.IonReaderBuilder;
   
   import java.io.IOException;
   import java.util.ArrayList;
   import java.util.Collections;
   import java.util.LinkedHashMap;
   import java.util.List;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.Owner;
   import software.amazon.qldb.tutorial.model.Person;
   import software.amazon.qldb.tutorial.model.SampleData;
   
   /**
    * Find primary owner for a particular vehicle's VIN.
    * Transfer to another primary owner for a particular vehicle's VIN.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class TransferVehicleOwnership {
       public static final Logger log = LoggerFactory.getLogger(TransferVehicleOwnership.class);
   
       private TransferVehicleOwnership() { }
   
       /**
        * Query a driver's information using the given ID.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param documentId
        *              The unique ID of a document in the Person table.
        * @return a {@link Person} object.
        * @throws IllegalStateException if failed to convert parameter into {@link IonValue}.
        */
       public static Person findPersonFromDocumentId(final TransactionExecutor txn, final String documentId) {
           try {
               log.info("Finding person for documentId: {}...", documentId);
               final String query = "SELECT p.* FROM Person AS p BY pid WHERE pid = ?";
   
               Result result = txn.execute(query, Constants.MAPPER.writeValueAsIonValue(documentId));
               if (result.isEmpty()) {
                   throw new IllegalStateException("Unable to find person with ID: " + documentId);
               }
   
               return Constants.MAPPER.readValue(result.iterator().next(), Person.class);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Find the primary owner for the given VIN.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              Unique VIN for a vehicle.
        * @return a {@link Person} object.
        * @throws IllegalStateException if failed to convert parameter into {@link IonValue}.
        */
       public static Person findPrimaryOwnerForVehicle(final TransactionExecutor txn, final String vin) {
           try {
               log.info("Finding primary owner for vehicle with Vin: {}...", vin);
               final String query = "SELECT Owners.PrimaryOwner.PersonId FROM VehicleRegistration AS v WHERE v.VIN = ?";
               final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(vin));
               Result result = txn.execute(query, parameters);
               final List<IonStruct> documents = ScanTable.toIonStructs(result);
               ScanTable.printDocuments(documents);
               if (documents.isEmpty()) {
                   throw new IllegalStateException("Unable to find registrations with VIN: " + vin);
               }
   
               final IonReader reader = IonReaderBuilder.standard().build(documents.get(0));
               final String personId = Constants.MAPPER.readValue(reader, LinkedHashMap.class).get("PersonId").toString();
               return findPersonFromDocumentId(txn, personId);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Update the primary owner for a vehicle registration with the given documentId.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              Unique VIN for a vehicle.
        * @param documentId
        *              New PersonId for the primary owner.
        * @throws IllegalStateException if no vehicle registration was found using the given document ID and VIN, or if failed
        * to convert parameters into {@link IonValue}.
        */
       public static void updateVehicleRegistration(final TransactionExecutor txn, final String vin, final String documentId) {
           try {
               log.info("Updating primary owner for vehicle with Vin: {}...", vin);
               final String query = "UPDATE VehicleRegistration AS v SET v.Owners.PrimaryOwner = ? WHERE v.VIN = ?";
   
               final List<IonValue> parameters = new ArrayList<>();
               parameters.add(Constants.MAPPER.writeValueAsIonValue(new Owner(documentId)));
               parameters.add(Constants.MAPPER.writeValueAsIonValue(vin));
   
               Result result = txn.execute(query, parameters);
               ScanTable.printDocuments(result);
               if (result.isEmpty()) {
                   throw new IllegalStateException("Unable to transfer vehicle, could not find registration.");
               } else {
                   log.info("Successfully transferred vehicle with VIN '{}' to new owner.", vin);
               }
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       public static void main(final String... args) {
           final String vin = SampleData.VEHICLES.get(0).getVin();
           final String primaryOwnerGovId = SampleData.PEOPLE.get(0).getGovId();
           final String newPrimaryOwnerGovId = SampleData.PEOPLE.get(1).getGovId();
   
           ConnectToLedger.getDriver().execute(txn -> {
               final Person primaryOwner = findPrimaryOwnerForVehicle(txn, vin);
               if (!primaryOwner.getGovId().equals(primaryOwnerGovId)) {
                   // Verify the primary owner.
                   throw new IllegalStateException("Incorrect primary owner identified for vehicle, unable to transfer.");
               }
   
               final String newOwner = Person.getDocumentIdByGovId(txn, newPrimaryOwnerGovId);
               updateVehicleRegistration(txn, vin, newOwner);
           });
           log.info("Successfully transferred vehicle ownership!");
       }
   }
   ```

------
#### [ 1.x ]

   ```
   /*
    * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazon.ion.IonReader;
   import com.amazon.ion.IonStruct;
   import com.amazon.ion.IonValue;
   import com.amazon.ion.system.IonReaderBuilder;
   
   import java.io.IOException;
   import java.util.ArrayList;
   import java.util.Collections;
   import java.util.LinkedHashMap;
   import java.util.List;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.Owner;
   import software.amazon.qldb.tutorial.model.Person;
   import software.amazon.qldb.tutorial.model.SampleData;
   
   /**
    * Find primary owner for a particular vehicle's VIN.
    * Transfer to another primary owner for a particular vehicle's VIN.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class TransferVehicleOwnership {
       public static final Logger log = LoggerFactory.getLogger(TransferVehicleOwnership.class);
   
       private TransferVehicleOwnership() { }
   
       /**
        * Query a driver's information using the given ID.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param documentId
        *              The unique ID of a document in the Person table.
        * @return a {@link Person} object.
        * @throws IllegalStateException if failed to convert parameter into {@link IonValue}.
        */
       public static Person findPersonFromDocumentId(final TransactionExecutor txn, final String documentId) {
           try {
               log.info("Finding person for documentId: {}...", documentId);
               final String query = "SELECT p.* FROM Person AS p BY pid WHERE pid = ?";
   
               Result result = txn.execute(query, Constants.MAPPER.writeValueAsIonValue(documentId));
               if (result.isEmpty()) {
                   throw new IllegalStateException("Unable to find person with ID: " + documentId);
               }
   
               return Constants.MAPPER.readValue(result.iterator().next(), Person.class);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Find the primary owner for the given VIN.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              Unique VIN for a vehicle.
        * @return a {@link Person} object.
        * @throws IllegalStateException if failed to convert parameter into {@link IonValue}.
        */
       public static Person findPrimaryOwnerForVehicle(final TransactionExecutor txn, final String vin) {
           try {
               log.info("Finding primary owner for vehicle with Vin: {}...", vin);
               final String query = "SELECT Owners.PrimaryOwner.PersonId FROM VehicleRegistration AS v WHERE v.VIN = ?";
               final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(vin));
               Result result = txn.execute(query, parameters);
               final List<IonStruct> documents = ScanTable.toIonStructs(result);
               ScanTable.printDocuments(documents);
               if (documents.isEmpty()) {
                   throw new IllegalStateException("Unable to find registrations with VIN: " + vin);
               }
   
               final IonReader reader = IonReaderBuilder.standard().build(documents.get(0));
               final String personId = Constants.MAPPER.readValue(reader, LinkedHashMap.class).get("PersonId").toString();
               return findPersonFromDocumentId(txn, personId);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Update the primary owner for a vehicle registration with the given documentId.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              Unique VIN for a vehicle.
        * @param documentId
        *              New PersonId for the primary owner.
        * @throws IllegalStateException if no vehicle registration was found using the given document ID and VIN, or if failed
        * to convert parameters into {@link IonValue}.
        */
       public static void updateVehicleRegistration(final TransactionExecutor txn, final String vin, final String documentId) {
           try {
               log.info("Updating primary owner for vehicle with Vin: {}...", vin);
               final String query = "UPDATE VehicleRegistration AS v SET v.Owners.PrimaryOwner = ? WHERE v.VIN = ?";
   
               final List<IonValue> parameters = new ArrayList<>();
               parameters.add(Constants.MAPPER.writeValueAsIonValue(new Owner(documentId)));
               parameters.add(Constants.MAPPER.writeValueAsIonValue(vin));
   
               Result result = txn.execute(query, parameters);
               ScanTable.printDocuments(result);
               if (result.isEmpty()) {
                   throw new IllegalStateException("Unable to transfer vehicle, could not find registration.");
               } else {
                   log.info("Successfully transferred vehicle with VIN '{}' to new owner.", vin);
               }
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       public static void main(final String... args) {
           final String vin = SampleData.VEHICLES.get(0).getVin();
           final String primaryOwnerGovId = SampleData.PEOPLE.get(0).getGovId();
           final String newPrimaryOwnerGovId = SampleData.PEOPLE.get(1).getGovId();
   
           ConnectToLedger.getDriver().execute(txn -> {
               final Person primaryOwner = findPrimaryOwnerForVehicle(txn, vin);
               if (!primaryOwner.getGovId().equals(primaryOwnerGovId)) {
                   // Verify the primary owner.
                   throw new IllegalStateException("Incorrect primary owner identified for vehicle, unable to transfer.");
               }
   
               final String newOwner = Person.getDocumentIdByGovId(txn, newPrimaryOwnerGovId);
               updateVehicleRegistration(txn, vin, newOwner);
           }, (retryAttempt) -> log.info("Retrying due to OCC conflict..."));
           log.info("Successfully transferred vehicle ownership!");
       }
   }
   ```

------

1. 编译并运行以下程序（`AddSecondaryOwner.java`），将二级车主添加至分类账中带有 VIN `KM8SRDHF6EU074761` 的车辆中。

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import java.io.IOException;
   import java.util.Collections;
   import java.util.Iterator;
   import java.util.List;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import com.amazon.ion.IonValue;
   
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.Owner;
   import software.amazon.qldb.tutorial.model.Owners;
   import software.amazon.qldb.tutorial.model.Person;
   import software.amazon.qldb.tutorial.model.SampleData;
   
   /**
    * Finds and adds secondary owners for a vehicle.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class AddSecondaryOwner {
       public static final Logger log = LoggerFactory.getLogger(AddSecondaryOwner.class);
   
       private AddSecondaryOwner() { }
   
       /**
        * Check whether a secondary owner has already been registered for the given VIN.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              Unique VIN for a vehicle.
        * @param secondaryOwnerId
        *              The secondary owner to add.
        * @return {@code true} if the given secondary owner has already been registered, {@code false} otherwise.
        * @throws IllegalStateException if failed to convert VIN to an {@link IonValue}.
        */
       public static boolean isSecondaryOwnerForVehicle(final TransactionExecutor txn, final String vin,
                                                        final String secondaryOwnerId) {
           try {
               log.info("Finding secondary owners for vehicle with VIN: {}...", vin);
               final String query = "SELECT Owners.SecondaryOwners FROM VehicleRegistration AS v WHERE v.VIN = ?";
               final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(vin));
               final Result result = txn.execute(query, parameters);
               final Iterator<IonValue> itr = result.iterator();
               if (!itr.hasNext()) {
                   return false;
               }
   
               final Owners owners = Constants.MAPPER.readValue(itr.next(), Owners.class);
               if (null != owners.getSecondaryOwners()) {
                   for (Owner owner : owners.getSecondaryOwners()) {
                       if (secondaryOwnerId.equalsIgnoreCase(owner.getPersonId())) {
                           return true;
                       }
                   }
               }
   
               return false;
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Adds a secondary owner for the specified VIN.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              Unique VIN for a vehicle.
        * @param secondaryOwner
        *              The secondary owner to add.
        * @throws IllegalStateException if failed to convert parameter into an {@link IonValue}.
        */
       public static void addSecondaryOwnerForVin(final TransactionExecutor txn, final String vin,
                                                  final String secondaryOwner) {
           try {
               log.info("Inserting secondary owner for vehicle with VIN: {}...", vin);
               final String query = String.format("FROM VehicleRegistration AS v WHERE v.VIN = ?" +
                       "INSERT INTO v.Owners.SecondaryOwners VALUE ?");
               final IonValue newOwner = Constants.MAPPER.writeValueAsIonValue(new Owner(secondaryOwner));
               final IonValue vinAsIonValue = Constants.MAPPER.writeValueAsIonValue(vin);
               Result result = txn.execute(query, vinAsIonValue, newOwner);
               log.info("VehicleRegistration Document IDs which had secondary owners added: ");
               ScanTable.printDocuments(result);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       public static void main(final String... args) {
           final String vin = SampleData.VEHICLES.get(1).getVin();
           final String govId = SampleData.PEOPLE.get(0).getGovId();
   
           ConnectToLedger.getDriver().execute(txn -> {
               final String documentId = Person.getDocumentIdByGovId(txn, govId);
               if (isSecondaryOwnerForVehicle(txn, vin, documentId)) {
                   log.info("Person with ID {} has already been added as a secondary owner of this vehicle.", govId);
               } else {
                   addSecondaryOwnerForVin(txn, vin, documentId);
               }
           });
           log.info("Secondary owners successfully updated.");
       }
   }
   ```

------
#### [ 1.x ]

   ```
   /*
    * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import java.io.IOException;
   import java.util.Collections;
   import java.util.Iterator;
   import java.util.List;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import com.amazon.ion.IonValue;
   
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.Owner;
   import software.amazon.qldb.tutorial.model.Owners;
   import software.amazon.qldb.tutorial.model.Person;
   import software.amazon.qldb.tutorial.model.SampleData;
   
   /**
    * Finds and adds secondary owners for a vehicle.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class AddSecondaryOwner {
       public static final Logger log = LoggerFactory.getLogger(AddSecondaryOwner.class);
   
       private AddSecondaryOwner() { }
   
       /**
        * Check whether a secondary owner has already been registered for the given VIN.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              Unique VIN for a vehicle.
        * @param secondaryOwnerId
        *              The secondary owner to add.
        * @return {@code true} if the given secondary owner has already been registered, {@code false} otherwise.
        * @throws IllegalStateException if failed to convert VIN to an {@link IonValue}.
        */
       public static boolean isSecondaryOwnerForVehicle(final TransactionExecutor txn, final String vin,
                                                        final String secondaryOwnerId) {
           try {
               log.info("Finding secondary owners for vehicle with VIN: {}...", vin);
               final String query = "SELECT Owners.SecondaryOwners FROM VehicleRegistration AS v WHERE v.VIN = ?";
               final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(vin));
               final Result result = txn.execute(query, parameters);
               final Iterator<IonValue> itr = result.iterator();
               if (!itr.hasNext()) {
                   return false;
               }
   
               final Owners owners = Constants.MAPPER.readValue(itr.next(), Owners.class);
               if (null != owners.getSecondaryOwners()) {
                   for (Owner owner : owners.getSecondaryOwners()) {
                       if (secondaryOwnerId.equalsIgnoreCase(owner.getPersonId())) {
                           return true;
                       }
                   }
               }
   
               return false;
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Adds a secondary owner for the specified VIN.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              Unique VIN for a vehicle.
        * @param secondaryOwner
        *              The secondary owner to add.
        * @throws IllegalStateException if failed to convert parameter into an {@link IonValue}.
        */
       public static void addSecondaryOwnerForVin(final TransactionExecutor txn, final String vin,
                                                  final String secondaryOwner) {
           try {
               log.info("Inserting secondary owner for vehicle with VIN: {}...", vin);
               final String query = String.format("FROM VehicleRegistration AS v WHERE v.VIN = '%s' " +
                       "INSERT INTO v.Owners.SecondaryOwners VALUE ?", vin);
               final IonValue newOwner = Constants.MAPPER.writeValueAsIonValue(new Owner(secondaryOwner));
               Result result = txn.execute(query, newOwner);
               log.info("VehicleRegistration Document IDs which had secondary owners added: ");
               ScanTable.printDocuments(result);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       public static void main(final String... args) {
           final String vin = SampleData.VEHICLES.get(1).getVin();
           final String govId = SampleData.PEOPLE.get(0).getGovId();
   
           ConnectToLedger.getDriver().execute(txn -> {
               final String documentId = Person.getDocumentIdByGovId(txn, govId);
               if (isSecondaryOwnerForVehicle(txn, vin, documentId)) {
                   log.info("Person with ID {} has already been added as a secondary owner of this vehicle.", govId);
               } else {
                   addSecondaryOwnerForVin(txn, vin, documentId);
               }
           }, (retryAttempt) -> log.info("Retrying due to OCC conflict..."));
           log.info("Secondary owners successfully updated.");
       }
   }
   ```

------

若要 查看 `vehicle-registration` 分类账中的这些更改，请参阅[步骤 6：查看文档修订历史记录](getting-started.java.step-6.md)。

# 步骤 6：查看文档修订历史记录
<a name="getting-started.java.step-6"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在上一步中修改车辆注册数据后，您可查询其所有注册车主的历史记录以及任何其他更新的字段。在此步骤中，您将在 `vehicle-registration` 分类账的 `VehicleRegistration` 表格中查询文档的修订历史记录。

**若要查看修订历史记录**

1. 查看以下程序（`QueryHistory.java`）。

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import java.io.IOException;
   import java.time.Instant;
   import java.time.temporal.ChronoUnit;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import com.amazon.ion.IonValue;
   
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.SampleData;
   import software.amazon.qldb.tutorial.model.VehicleRegistration;
   
   /**
    * Query a table's history for a particular set of documents.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class QueryHistory {
       public static final Logger log = LoggerFactory.getLogger(QueryHistory.class);
       private static final int THREE_MONTHS = 90;
   
       private QueryHistory() { }
   
       /**
        * In this example, query the 'VehicleRegistration' history table to find all previous primary owners for a VIN.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              VIN to find previous primary owners for.
        * @param query
        *              The query to find previous primary owners.
        * @throws IllegalStateException if failed to convert document ID to an {@link IonValue}.
        */
       public static void previousPrimaryOwners(final TransactionExecutor txn, final String vin, final String query) {
           try {
               final String docId = VehicleRegistration.getDocumentIdByVin(txn, vin);
   
               log.info("Querying the 'VehicleRegistration' table's history using VIN: {}...", vin);
               final Result result = txn.execute(query, Constants.MAPPER.writeValueAsIonValue(docId));
               ScanTable.printDocuments(result);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       public static void main(final String... args) {
           final String threeMonthsAgo = Instant.now().minus(THREE_MONTHS, ChronoUnit.DAYS).toString();
           final String query = String.format("SELECT data.Owners.PrimaryOwner, metadata.version "
                                              + "FROM history(VehicleRegistration, `%s`) "
                                              + "AS h WHERE h.metadata.id = ?", threeMonthsAgo);
           ConnectToLedger.getDriver().execute(txn -> {
               final String vin = SampleData.VEHICLES.get(0).getVin();
               previousPrimaryOwners(txn, vin, query);
           });
           log.info("Successfully queried history.");
       }
   }
   ```

------
#### [ 1.x ]

   ```
   /*
    * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazon.ion.IonValue;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   import software.amazon.qldb.QldbSession;
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.SampleData;
   import software.amazon.qldb.tutorial.model.VehicleRegistration;
   
   import java.io.IOException;
   import java.time.Instant;
   import java.time.temporal.ChronoUnit;
   import java.util.Collections;
   import java.util.List;
   
   /**
    * Query a table's history for a particular set of documents.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class QueryHistory {
       public static final Logger log = LoggerFactory.getLogger(QueryHistory.class);
       private static final int THREE_MONTHS = 90;
   
       private QueryHistory() { }
   
       /**
        * In this example, query the 'VehicleRegistration' history table to find all previous primary owners for a VIN.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              VIN to find previous primary owners for.
        * @param query
        *              The query to find previous primary owners.
        * @throws IllegalStateException if failed to convert document ID to an {@link IonValue}.
        */
       public static void previousPrimaryOwners(final TransactionExecutor txn, final String vin, final String query) {
           try {
               final String docId = VehicleRegistration.getDocumentIdByVin(txn, vin);
               final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(docId));
               log.info("Querying the 'VehicleRegistration' table's history using VIN: {}...", vin);
               final Result result = txn.execute(query, parameters);
               ScanTable.printDocuments(result);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       public static void main(final String... args) {
           final String threeMonthsAgo = Instant.now().minus(THREE_MONTHS, ChronoUnit.DAYS).toString();
           final String query = String.format("SELECT data.Owners.PrimaryOwner, metadata.version "
                                              + "FROM history(VehicleRegistration, `%s`) "
                                              + "AS h WHERE h.metadata.id = ?", threeMonthsAgo);
           ConnectToLedger.getDriver().execute(txn -> {
               final String vin = SampleData.VEHICLES.get(0).getVin();
               previousPrimaryOwners(txn, vin, query);
           }, (retryAttempt) -> log.info("Retrying due to OCC conflict..."));
           log.info("Successfully queried history.");
       }
   }
   ```

------
**注意**  
您可以通过以下语法查询内置版本，以查看文档[历史记录函数](working.history.md#working.history.function)的修订历史记录。  

     ```
     SELECT * FROM history( table_name [, `start-time` [, `end-time` ] ] ) AS h
     [ WHERE h.metadata.id = 'id' ]
     ```
*开始时间*和*结束时间*均为可选。它们是 Amazon Ion 的字面值，可以用反引号（``...``）表示。要了解更多信息，请参阅 [在 Amazon QLDB 中使用 PartiQL 查询 Ion](ql-reference.query.md)。
最佳做法是，使用日期范围（*开始时间* 和 *结束时间*）和文档 ID（`metadata.id`）来限定历史记录查询。QLDB 处理事务中的`SELECT`查询，这些查询受[事务超时限制](limits.md#limits.fixed)的约束。  
QLDB 历史记录按文档 ID 编制索引，目前无法创建其他历史索引。包含开始时间和结束时间的历史记录查询将从日期范围限定中获得便利。

1. 编译并运行 `QueryHistory.java` 程序，以使用 VIN `1N4AL11D75C109151`查询`VehicleRegistration`文档的修订历史记录。

要以加密方式验证 `vehicle-registration` 分类账中的文档修订版，请继续[第 7 步：验证分类账中的文档](getting-started.java.step-7.md)。

# 第 7 步：验证分类账中的文档
<a name="getting-started.java.step-7"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

借助 Amazon QLDB，您可在 SHA-256 中使用加密哈希，从而有效地验证分类账日记账中文档的完整性。要详细了解验证和加密哈希在 QLDB 中的工作原理，请参阅 [Amazon QLDB 中的数据验证](verification.md)。

在此步骤中，您将验证 `vehicle-registration` 分类账 `VehicleRegistration` 表格中的单据修订版本。首先，您请求一份摘要，该摘要作为输出文件返回，并作为分类账整个变更历史记录的签名。然后，您要求提供与此摘要相关的修订证明。使用此证明，如果所有验证检查都可通过，可以验证修订版的完整性。

**验证文档的修订版**

1. 查看以下 `.java` 文件，这些文件代表了验证所需 QLDB 对象，以及带有 Ion 和字符串值的辅助方法的实用程序类。

   1. `BlockAddress.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.qldb;
      
      import java.util.Objects;
      
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      
      import com.fasterxml.jackson.annotation.JsonCreator;
      import com.fasterxml.jackson.annotation.JsonProperty;
      
      /**
       * Represents the BlockAddress field of a QLDB document.
       */
      public final class BlockAddress {
      
          private static final Logger log = LoggerFactory.getLogger(BlockAddress.class);
      
          private final String strandId;
          private final long sequenceNo;
      
          @JsonCreator
          public BlockAddress(@JsonProperty("strandId") final String strandId,
                              @JsonProperty("sequenceNo") final long sequenceNo) {
              this.strandId = strandId;
              this.sequenceNo = sequenceNo;
          }
      
          public long getSequenceNo() {
              return sequenceNo;
          }
      
          public String getStrandId() {
              return strandId;
          }
      
          @Override
          public String toString() {
              return "BlockAddress{"
                      + "strandId='" + strandId + '\''
                      + ", sequenceNo=" + sequenceNo
                      + '}';
          }
      
          @Override
          public boolean equals(final Object o) {
              if (this == o) {
                  return true;
              }
              if (o == null || getClass() != o.getClass()) {
                  return false;
              }
              BlockAddress that = (BlockAddress) o;
              return sequenceNo == that.sequenceNo
                      && strandId.equals(that.strandId);
          }
      
          @Override
          public int hashCode() {
              // CHECKSTYLE:OFF - Disabling as we are generating a hashCode of multiple properties.
              return Objects.hash(strandId, sequenceNo);
              // CHECKSTYLE:ON
          }
      }
      ```

   1. `Proof.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.qldb;
      
      import com.amazon.ion.IonReader;
      import com.amazon.ion.IonSystem;
      import com.amazon.ion.system.IonSystemBuilder;
      import com.amazonaws.services.qldb.model.GetRevisionRequest;
      import com.amazonaws.services.qldb.model.GetRevisionResult;
      
      import java.util.ArrayList;
      import java.util.List;
      
      /**
       * A Java representation of the {@link Proof} object.
       * Returned from the {@link com.amazonaws.services.qldb.AmazonQLDB#getRevision(GetRevisionRequest)} api.
       */
      public final class Proof {
          private static final IonSystem SYSTEM = IonSystemBuilder.standard().build();
      
          private List<byte[]> internalHashes;
      
          public Proof(final List<byte[]> internalHashes) {
              this.internalHashes = internalHashes;
          }
      
          public List<byte[]> getInternalHashes() {
              return internalHashes;
          }
      
          /**
           * Decodes a {@link Proof} from an ion text String. This ion text is returned in
           * a {@link GetRevisionResult#getProof()}
           *
           * @param ionText
           *              The ion text representing a {@link Proof} object.
           * @return {@link JournalBlock} parsed from the ion text.
           * @throws IllegalStateException if failed to parse the {@link Proof} object from the given ion text.
           */
          public static Proof fromBlob(final String ionText) {
              try {
                  IonReader reader = SYSTEM.newReader(ionText);
                  List<byte[]> list = new ArrayList<>();
                  reader.next();
                  reader.stepIn();
                  while (reader.next() != null) {
                      list.add(reader.newBytes());
                  }
                  return new Proof(list);
              } catch (Exception e) {
                  throw new IllegalStateException("Failed to parse a Proof from byte array");
              }
          }
      }
      ```

   1. `QldbIonUtils.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.qldb;
      
      import com.amazon.ion.IonReader;
      import com.amazon.ion.IonValue;
      import com.amazon.ionhash.IonHashReader;
      import com.amazon.ionhash.IonHashReaderBuilder;
      import com.amazon.ionhash.MessageDigestIonHasherProvider;
      import software.amazon.qldb.tutorial.Constants;
      
      public class QldbIonUtils {
      
          private static MessageDigestIonHasherProvider ionHasherProvider = new MessageDigestIonHasherProvider("SHA-256");
      
          private QldbIonUtils() {}
      
          /**
           * Builds a hash value from the given {@link IonValue}.
           *
           * @param ionValue
           *              The {@link IonValue} to hash.
           * @return a byte array representing the hash value.
           */
          public static byte[] hashIonValue(final IonValue ionValue) {
              IonReader reader = Constants.SYSTEM.newReader(ionValue);
              IonHashReader hashReader = IonHashReaderBuilder.standard()
                      .withHasherProvider(ionHasherProvider)
                      .withReader(reader)
                      .build();
              while (hashReader.next() != null) {  }
              return hashReader.digest();
          }
      
      }
      ```

   1. `QldbStringUtils.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.qldb;
      
      import com.amazon.ion.IonWriter;
      import com.amazon.ion.system.IonReaderBuilder;
      import com.amazon.ion.system.IonTextWriterBuilder;
      import com.amazonaws.services.qldb.model.GetBlockResult;
      import com.amazonaws.services.qldb.model.GetDigestResult;
      import com.amazonaws.services.qldb.model.ValueHolder;
      
      import java.io.IOException;
      
      /**
       * Helper methods to pretty-print certain QLDB response types.
       */
      public class QldbStringUtils {
      
          private QldbStringUtils() {}
      
          /**
           * Returns the string representation of a given {@link ValueHolder}.
           * Adapted from the AWS SDK autogenerated {@code toString()} method, with sensitive values un-redacted.
           * Additionally, this method pretty-prints any IonText included in the {@link ValueHolder}.
           *
           * @param valueHolder the {@link ValueHolder} to convert to a String.
           * @return the String representation of the supplied {@link ValueHolder}.
           */
          public static String toUnredactedString(ValueHolder valueHolder) {
              StringBuilder sb = new StringBuilder();
              sb.append("{");
              if (valueHolder.getIonText() != null) {
      
                  sb.append("IonText: ");
                  IonWriter prettyWriter = IonTextWriterBuilder.pretty().build(sb);
                  try {
                      prettyWriter.writeValues(IonReaderBuilder.standard().build(valueHolder.getIonText()));
                  } catch (IOException ioe) {
                      sb.append("**Exception while printing this IonText**");
                  }
              }
      
              sb.append("}");
              return sb.toString();
          }
      
          /**
           * Returns the string representation of a given {@link GetBlockResult}.
           * Adapted from the AWS SDK autogenerated {@code toString()} method, with sensitive values un-redacted.
           *
           * @param getBlockResult the {@link GetBlockResult} to convert to a String.
           * @return the String representation of the supplied {@link GetBlockResult}.
           */
          public static String toUnredactedString(GetBlockResult getBlockResult) {
              StringBuilder sb = new StringBuilder();
              sb.append("{");
              if (getBlockResult.getBlock() != null) {
                  sb.append("Block: ").append(toUnredactedString(getBlockResult.getBlock())).append(",");
              }
      
              if (getBlockResult.getProof() != null) {
                  sb.append("Proof: ").append(toUnredactedString(getBlockResult.getProof()));
              }
      
              sb.append("}");
              return sb.toString();
          }
      
          /**
           * Returns the string representation of a given {@link GetDigestResult}.
           * Adapted from the AWS SDK autogenerated {@code toString()} method, with sensitive values un-redacted.
           *
           * @param getDigestResult the {@link GetDigestResult} to convert to a String.
           * @return the String representation of the supplied {@link GetDigestResult}.
           */
          public static String toUnredactedString(GetDigestResult getDigestResult) {
              StringBuilder sb = new StringBuilder();
              sb.append("{");
              if (getDigestResult.getDigest() != null) {
                  sb.append("Digest: ").append(getDigestResult.getDigest()).append(",");
              }
      
              if (getDigestResult.getDigestTipAddress() != null) {
                  sb.append("DigestTipAddress: ").append(toUnredactedString(getDigestResult.getDigestTipAddress()));
              }
      
              sb.append("}");
              return sb.toString();
          }
      }
      ```

   1. `Verifier.java`

------
#### [ 2.x ]

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial;
      
      import java.nio.ByteBuffer;
      import java.nio.charset.StandardCharsets;
      import java.security.MessageDigest;
      import java.security.NoSuchAlgorithmException;
      import java.util.ArrayList;
      import java.util.Arrays;
      import java.util.Comparator;
      import java.util.Iterator;
      import java.util.List;
      import java.util.concurrent.ThreadLocalRandom;
      
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      
      import com.amazonaws.util.Base64;
      
      import software.amazon.qldb.tutorial.qldb.Proof;
      
      /**
       * Encapsulates the logic to verify the integrity of revisions or blocks in a QLDB ledger.
       *
       * The main entry point is {@link #verify(byte[], byte[], String)}.
       *
       * This code expects that you have AWS credentials setup per:
       * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
       */
      public final class Verifier {
          public static final Logger log = LoggerFactory.getLogger(Verifier.class);
          private static final int HASH_LENGTH = 32;
          private static final int UPPER_BOUND = 8;
      
          /**
           * Compares two hashes by their <em>signed</em> byte values in little-endian order.
           */
          private static Comparator<byte[]> hashComparator = (h1, h2) -> {
              if (h1.length != HASH_LENGTH || h2.length != HASH_LENGTH) {
                  throw new IllegalArgumentException("Invalid hash.");
              }
              for (int i = h1.length - 1; i >= 0; i--) {
                  int byteEqual = Byte.compare(h1[i], h2[i]);
                  if (byteEqual != 0) {
                      return byteEqual;
                  }
              }
      
              return 0;
          };
      
          private Verifier() { }
      
          /**
           * Verify the integrity of a document with respect to a QLDB ledger digest.
           *
           * The verification algorithm includes the following steps:
           *
           * 1. {@link #buildCandidateDigest(Proof, byte[])} build the candidate digest from the internal hashes
           * in the {@link Proof}.
           * 2. Check that the {@code candidateLedgerDigest} is equal to the {@code ledgerDigest}.
           *
           * @param documentHash
           *              The hash of the document to be verified.
           * @param digest
           *              The QLDB ledger digest. This digest should have been retrieved using
           *              {@link com.amazonaws.services.qldb.AmazonQLDB#getDigest}
           * @param proofBlob
           *              The ion encoded bytes representing the {@link Proof} associated with the supplied
           *              {@code digestTipAddress} and {@code address} retrieved using
           *              {@link com.amazonaws.services.qldb.AmazonQLDB#getRevision}.
           * @return {@code true} if the record is verified or {@code false} if it is not verified.
           */
          public static boolean verify(
                  final byte[] documentHash,
                  final byte[] digest,
                  final String proofBlob
          ) {
              Proof proof = Proof.fromBlob(proofBlob);
      
              byte[] candidateDigest = buildCandidateDigest(proof, documentHash);
      
              return Arrays.equals(digest, candidateDigest);
          }
      
          /**
           * Build the candidate digest representing the entire ledger from the internal hashes of the {@link Proof}.
           *
           * @param proof
           *              A Java representation of {@link Proof}
           *              returned from {@link com.amazonaws.services.qldb.AmazonQLDB#getRevision}.
           * @param leafHash
           *              Leaf hash to build the candidate digest with.
           * @return a byte array of the candidate digest.
           */
          private static byte[] buildCandidateDigest(final Proof proof, final byte[] leafHash) {
              return calculateRootHashFromInternalHashes(proof.getInternalHashes(), leafHash);
          }
      
          /**
           * Get a new instance of {@link MessageDigest} using the SHA-256 algorithm.
           *
           * @return an instance of {@link MessageDigest}.
           * @throws IllegalStateException if the algorithm is not available on the current JVM.
           */
          static MessageDigest newMessageDigest() {
              try {
                  return MessageDigest.getInstance("SHA-256");
              } catch (NoSuchAlgorithmException e) {
                  log.error("Failed to create SHA-256 MessageDigest", e);
                  throw new IllegalStateException("SHA-256 message digest is unavailable", e);
              }
          }
      
          /**
           * Takes two hashes, sorts them, concatenates them, and then returns the
           * hash of the concatenated array.
           *
           * @param h1
           *              Byte array containing one of the hashes to compare.
           * @param h2
           *              Byte array containing one of the hashes to compare.
           * @return the concatenated array of hashes.
           */
          public static byte[] dot(final byte[] h1, final byte[] h2) {
              if (h1.length == 0) {
                  return h2;
              }
              if (h2.length == 0) {
                  return h1;
              }
              byte[] concatenated = new byte[h1.length + h2.length];
              if (hashComparator.compare(h1, h2) < 0) {
                  System.arraycopy(h1, 0, concatenated, 0, h1.length);
                  System.arraycopy(h2, 0, concatenated, h1.length, h2.length);
              } else {
                  System.arraycopy(h2, 0, concatenated, 0, h2.length);
                  System.arraycopy(h1, 0, concatenated, h2.length, h1.length);
              }
              MessageDigest messageDigest = newMessageDigest();
              messageDigest.update(concatenated);
      
              return messageDigest.digest();
          }
      
          /**
           * Starting with the provided {@code leafHash} combined with the provided {@code internalHashes}
           * pairwise until only the root hash remains.
           *
           * @param internalHashes
           *              Internal hashes of Merkle tree.
           * @param leafHash
           *              Leaf hashes of Merkle tree.
           * @return the root hash.
           */
          private static byte[] calculateRootHashFromInternalHashes(final List<byte[]> internalHashes, final byte[] leafHash) {
              return internalHashes.stream().reduce(leafHash, Verifier::dot);
          }
      
          /**
           * Flip a single random bit in the given byte array. This method is used to demonstrate
           * QLDB's verification features.
           *
           * @param original
           *              The original byte array.
           * @return the altered byte array with a single random bit changed.
           */
          public static byte[] flipRandomBit(final byte[] original) {
              if (original.length == 0) {
                  throw new IllegalArgumentException("Array cannot be empty!");
              }
              int alteredPosition = ThreadLocalRandom.current().nextInt(original.length);
              int b = ThreadLocalRandom.current().nextInt(UPPER_BOUND);
              byte[] altered = new byte[original.length];
              System.arraycopy(original, 0, altered, 0, original.length);
              altered[alteredPosition] = (byte) (altered[alteredPosition] ^ (1 << b));
              return altered;
          }
      
          public static String toBase64(byte[] arr) {
              return new String(Base64.encode(arr), StandardCharsets.UTF_8);
          }
      
          /**
           * Convert a {@link ByteBuffer} into byte array.
           *
           * @param buffer
           *              The {@link ByteBuffer} to convert.
           * @return the converted byte array.
           */
          public static byte[] convertByteBufferToByteArray(final ByteBuffer buffer) {
              byte[] arr = new byte[buffer.remaining()];
              buffer.get(arr);
              return arr;
          }
      
          /**
           * Calculates the root hash from a list of hashes that represent the base of a Merkle tree.
           *
           * @param hashes
           *              The list of byte arrays representing hashes making up base of a Merkle tree.
           * @return a byte array that is the root hash of the given list of hashes.
           */
          public static byte[] calculateMerkleTreeRootHash(List<byte[]> hashes) {
              if (hashes.isEmpty()) {
                  return new byte[0];
              }
      
              List<byte[]> remaining = combineLeafHashes(hashes);
              while (remaining.size() > 1) {
                  remaining = combineLeafHashes(remaining);
              }
              return remaining.get(0);
          }
      
          private static List<byte[]> combineLeafHashes(List<byte[]> hashes) {
              List<byte[]> combinedHashes = new ArrayList<>();
              Iterator<byte[]> it = hashes.stream().iterator();
      
              while (it.hasNext()) {
                  byte[] left = it.next();
                  if (it.hasNext()) {
                      byte[] right = it.next();
                      byte[] combined = dot(left, right);
                      combinedHashes.add(combined);
                  } else {
                      combinedHashes.add(left);
                  }
              }
      
              return combinedHashes;
          }
      }
      ```

------
#### [ 1.x ]

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial;
      
      import com.amazonaws.util.Base64;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      import software.amazon.qldb.tutorial.qldb.Proof;
      
      import java.nio.ByteBuffer;
      import java.nio.charset.StandardCharsets;
      import java.security.MessageDigest;
      import java.security.NoSuchAlgorithmException;
      import java.util.*;
      import java.util.concurrent.ThreadLocalRandom;
      
      /**
       * Encapsulates the logic to verify the integrity of revisions or blocks in a QLDB ledger.
       *
       * The main entry point is {@link #verify(byte[], byte[], String)}.
       *
       * This code expects that you have AWS credentials setup per:
       * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
       */
      public final class Verifier {
          public static final Logger log = LoggerFactory.getLogger(Verifier.class);
          private static final int HASH_LENGTH = 32;
          private static final int UPPER_BOUND = 8;
      
          /**
           * Compares two hashes by their <em>signed</em> byte values in little-endian order.
           */
          private static Comparator<byte[]> hashComparator = (h1, h2) -> {
              if (h1.length != HASH_LENGTH || h2.length != HASH_LENGTH) {
                  throw new IllegalArgumentException("Invalid hash.");
              }
              for (int i = h1.length - 1; i >= 0; i--) {
                  int byteEqual = Byte.compare(h1[i], h2[i]);
                  if (byteEqual != 0) {
                      return byteEqual;
                  }
              }
      
              return 0;
          };
      
          private Verifier() { }
      
          /**
           * Verify the integrity of a document with respect to a QLDB ledger digest.
           *
           * The verification algorithm includes the following steps:
           *
           * 1. {@link #buildCandidateDigest(Proof, byte[])} build the candidate digest from the internal hashes
           * in the {@link Proof}.
           * 2. Check that the {@code candidateLedgerDigest} is equal to the {@code ledgerDigest}.
           *
           * @param documentHash
           *              The hash of the document to be verified.
           * @param digest
           *              The QLDB ledger digest. This digest should have been retrieved using
           *              {@link com.amazonaws.services.qldb.AmazonQLDB#getDigest}
           * @param proofBlob
           *              The ion encoded bytes representing the {@link Proof} associated with the supplied
           *              {@code digestTipAddress} and {@code address} retrieved using
           *              {@link com.amazonaws.services.qldb.AmazonQLDB#getRevision}.
           * @return {@code true} if the record is verified or {@code false} if it is not verified.
           */
          public static boolean verify(
                  final byte[] documentHash,
                  final byte[] digest,
                  final String proofBlob
          ) {
              Proof proof = Proof.fromBlob(proofBlob);
      
              byte[] candidateDigest = buildCandidateDigest(proof, documentHash);
      
              return Arrays.equals(digest, candidateDigest);
          }
      
          /**
           * Build the candidate digest representing the entire ledger from the internal hashes of the {@link Proof}.
           *
           * @param proof
           *              A Java representation of {@link Proof}
           *              returned from {@link com.amazonaws.services.qldb.AmazonQLDB#getRevision}.
           * @param leafHash
           *              Leaf hash to build the candidate digest with.
           * @return a byte array of the candidate digest.
           */
          private static byte[] buildCandidateDigest(final Proof proof, final byte[] leafHash) {
              return calculateRootHashFromInternalHashes(proof.getInternalHashes(), leafHash);
          }
      
          /**
           * Get a new instance of {@link MessageDigest} using the SHA-256 algorithm.
           *
           * @return an instance of {@link MessageDigest}.
           * @throws IllegalStateException if the algorithm is not available on the current JVM.
           */
          static MessageDigest newMessageDigest() {
              try {
                  return MessageDigest.getInstance("SHA-256");
              } catch (NoSuchAlgorithmException e) {
                  log.error("Failed to create SHA-256 MessageDigest", e);
                  throw new IllegalStateException("SHA-256 message digest is unavailable", e);
              }
          }
      
          /**
           * Takes two hashes, sorts them, concatenates them, and then returns the
           * hash of the concatenated array.
           *
           * @param h1
           *              Byte array containing one of the hashes to compare.
           * @param h2
           *              Byte array containing one of the hashes to compare.
           * @return the concatenated array of hashes.
           */
          public static byte[] dot(final byte[] h1, final byte[] h2) {
              if (h1.length == 0) {
                  return h2;
              }
              if (h2.length == 0) {
                  return h1;
              }
              byte[] concatenated = new byte[h1.length + h2.length];
              if (hashComparator.compare(h1, h2) < 0) {
                  System.arraycopy(h1, 0, concatenated, 0, h1.length);
                  System.arraycopy(h2, 0, concatenated, h1.length, h2.length);
              } else {
                  System.arraycopy(h2, 0, concatenated, 0, h2.length);
                  System.arraycopy(h1, 0, concatenated, h2.length, h1.length);
              }
              MessageDigest messageDigest = newMessageDigest();
              messageDigest.update(concatenated);
      
              return messageDigest.digest();
          }
      
          /**
           * Starting with the provided {@code leafHash} combined with the provided {@code internalHashes}
           * pairwise until only the root hash remains.
           *
           * @param internalHashes
           *              Internal hashes of Merkle tree.
           * @param leafHash
           *              Leaf hashes of Merkle tree.
           * @return the root hash.
           */
          private static byte[] calculateRootHashFromInternalHashes(final List<byte[]> internalHashes, final byte[] leafHash) {
              return internalHashes.stream().reduce(leafHash, Verifier::dot);
          }
      
          /**
           * Flip a single random bit in the given byte array. This method is used to demonstrate
           * QLDB's verification features.
           *
           * @param original
           *              The original byte array.
           * @return the altered byte array with a single random bit changed.
           */
          public static byte[] flipRandomBit(final byte[] original) {
              if (original.length == 0) {
                  throw new IllegalArgumentException("Array cannot be empty!");
              }
              int alteredPosition = ThreadLocalRandom.current().nextInt(original.length);
              int b = ThreadLocalRandom.current().nextInt(UPPER_BOUND);
              byte[] altered = new byte[original.length];
              System.arraycopy(original, 0, altered, 0, original.length);
              altered[alteredPosition] = (byte) (altered[alteredPosition] ^ (1 << b));
              return altered;
          }
      
          public static String toBase64(byte[] arr) {
              return new String(Base64.encode(arr), StandardCharsets.UTF_8);
          }
      
          /**
           * Convert a {@link ByteBuffer} into byte array.
           *
           * @param buffer
           *              The {@link ByteBuffer} to convert.
           * @return the converted byte array.
           */
          public static byte[] convertByteBufferToByteArray(final ByteBuffer buffer) {
              byte[] arr = new byte[buffer.remaining()];
              buffer.get(arr);
              return arr;
          }
      
          /**
           * Calculates the root hash from a list of hashes that represent the base of a Merkle tree.
           *
           * @param hashes
           *              The list of byte arrays representing hashes making up base of a Merkle tree.
           * @return a byte array that is the root hash of the given list of hashes.
           */
          public static byte[] calculateMerkleTreeRootHash(List<byte[]> hashes) {
              if (hashes.isEmpty()) {
                  return new byte[0];
              }
      
              List<byte[]> remaining = combineLeafHashes(hashes);
              while (remaining.size() > 1) {
                  remaining = combineLeafHashes(remaining);
              }
              return remaining.get(0);
          }
      
          private static List<byte[]> combineLeafHashes(List<byte[]> hashes) {
              List<byte[]> combinedHashes = new ArrayList<>();
              Iterator<byte[]> it = hashes.stream().iterator();
      
              while (it.hasNext()) {
                  byte[] left = it.next();
                  if (it.hasNext()) {
                      byte[] right = it.next();
                      byte[] combined = dot(left, right);
                      combinedHashes.add(combined);
                  } else {
                      combinedHashes.add(left);
                  }
              }
      
              return combinedHashes;
          }
      }
      ```

------

1. 使用两个`.java`文件（`GetDigest.java`和`GetRevision.java`）执行以下步骤：
   + 从`vehicle-registration`分类账中请求新摘要。
   + 请索取 `VehicleRegistration` 表格中每份文件修订版的证明。
   + 通过重新计算摘要，使用返回的摘要和证明验证修订版。

   `GetDigest.java` 程序包含以下代码。

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazonaws.services.qldb.AmazonQLDB;
   import com.amazonaws.services.qldb.model.GetDigestRequest;
   import com.amazonaws.services.qldb.model.GetDigestResult;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   import software.amazon.qldb.tutorial.qldb.QldbStringUtils;
   
   /**
    * This is an example for retrieving the digest of a particular ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class GetDigest {
       public static final Logger log = LoggerFactory.getLogger(GetDigest.class);
       public static AmazonQLDB client = CreateLedger.getClient();
   
       private GetDigest() { }
   
       /**
        * Calls {@link #getDigest(String)} for a ledger.
        *
        * @param args
        *              Arbitrary command-line arguments.
        * @throws Exception if failed to get a ledger digest.
        */
       public static void main(final String... args) throws Exception {
           try {
   
               getDigest(Constants.LEDGER_NAME);
   
           } catch (Exception e) {
               log.error("Unable to get a ledger digest!", e);
               throw e;
           }
       }
   
       /**
        * Get the digest for the specified ledger.
        *
        * @param ledgerName
        *              The ledger to get digest from.
        * @return {@link GetDigestResult}.
        */
       public static GetDigestResult getDigest(final String ledgerName) {
           log.info("Let's get the current digest of the ledger named {}.", ledgerName);
           GetDigestRequest request = new GetDigestRequest()
                   .withName(ledgerName);
           GetDigestResult result = client.getDigest(request);
           log.info("Success. LedgerDigest: {}.", QldbStringUtils.toUnredactedString(result));
           return result;
       }
   }
   ```
**注意**  
使用该 `getDigest` 方法请求一份涵盖分类账中日记账当前*提示*的摘要。日记账提示指的是 QLDB 收到您的请求时的最近提交的数据块。

   `GetRevision.java` 程序包含以下代码。

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazon.ion.IonReader;
   import com.amazon.ion.IonStruct;
   import com.amazon.ion.IonSystem;
   import com.amazon.ion.IonValue;
   import com.amazon.ion.IonWriter;
   import com.amazon.ion.system.IonReaderBuilder;
   import com.amazon.ion.system.IonSystemBuilder;
   import com.amazonaws.services.qldb.AmazonQLDB;
   import com.amazonaws.services.qldb.model.GetDigestResult;
   import com.amazonaws.services.qldb.model.GetRevisionRequest;
   import com.amazonaws.services.qldb.model.GetRevisionResult;
   import com.amazonaws.services.qldb.model.ValueHolder;
   import java.io.ByteArrayOutputStream;
   import java.io.IOException;
   import java.util.ArrayList;
   import java.util.Collections;
   import java.util.List;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   import software.amazon.qldb.QldbDriver;
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.SampleData;
   import software.amazon.qldb.tutorial.qldb.BlockAddress;
   import software.amazon.qldb.tutorial.qldb.QldbRevision;
   import software.amazon.qldb.tutorial.qldb.QldbStringUtils;
   
   /**
    * Verify the integrity of a document revision in a QLDB ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class GetRevision {
       public static final Logger log = LoggerFactory.getLogger(GetRevision.class);
       public static AmazonQLDB client = CreateLedger.getClient();
       private static final IonSystem SYSTEM = IonSystemBuilder.standard().build();
   
       private GetRevision() { }
   
       public static void main(String... args) throws Exception {
   
           final String vin = SampleData.REGISTRATIONS.get(0).getVin();
   
   
           verifyRegistration(ConnectToLedger.getDriver(), Constants.LEDGER_NAME, vin);
       }
   
       /**
        * Verify each version of the registration for the given VIN.
        *
        * @param driver
        *              A QLDB driver.
        * @param ledgerName
        *              The ledger to get digest from.
        * @param vin
        *              VIN to query the revision history of a specific registration with.
        * @throws Exception if failed to verify digests.
        * @throws AssertionError if document revision verification failed.
        */
       public static void verifyRegistration(final QldbDriver driver, final String ledgerName, final String vin)
               throws Exception {
           log.info(String.format("Let's verify the registration with VIN=%s, in ledger=%s.", vin, ledgerName));
   
           try {
               log.info("First, let's get a digest.");
               GetDigestResult digestResult = GetDigest.getDigest(ledgerName);
   
               ValueHolder digestTipAddress = digestResult.getDigestTipAddress();
               byte[] digestBytes = Verifier.convertByteBufferToByteArray(digestResult.getDigest());
   
               log.info("Got a ledger digest. Digest end address={}, digest={}.",
                   QldbStringUtils.toUnredactedString(digestTipAddress),
                   Verifier.toBase64(digestBytes));
   
               log.info(String.format("Next, let's query the registration with VIN=%s. "
                       + "Then we can verify each version of the registration.", vin));
               List<IonStruct> documentsWithMetadataList = new ArrayList<>();
               driver.execute(txn -> {
                   documentsWithMetadataList.addAll(queryRegistrationsByVin(txn, vin));
               });
               log.info("Registrations queried successfully!");
   
               log.info(String.format("Found %s revisions of the registration with VIN=%s.",
                       documentsWithMetadataList.size(), vin));
   
               for (IonStruct ionStruct : documentsWithMetadataList) {
   
                   QldbRevision document = QldbRevision.fromIon(ionStruct);
                   log.info(String.format("Let's verify the document: %s", document));
   
                   log.info("Let's get a proof for the document.");
                   GetRevisionResult proofResult = getRevision(
                           ledgerName,
                           document.getMetadata().getId(),
                           digestTipAddress,
                           document.getBlockAddress()
                   );
   
                   final IonValue proof = Constants.MAPPER.writeValueAsIonValue(proofResult.getProof());
                   final IonReader reader = IonReaderBuilder.standard().build(proof);
                   reader.next();
                   ByteArrayOutputStream baos = new ByteArrayOutputStream();
                   IonWriter writer = SYSTEM.newBinaryWriter(baos);
                   writer.writeValue(reader);
                   writer.close();
                   baos.flush();
                   baos.close();
                   byte[] byteProof = baos.toByteArray();
   
                   log.info(String.format("Got back a proof: %s", Verifier.toBase64(byteProof)));
   
                   boolean verified = Verifier.verify(
                           document.getHash(),
                           digestBytes,
                           proofResult.getProof().getIonText()
                   );
   
                   if (!verified) {
                       throw new AssertionError("Document revision is not verified!");
                   } else {
                       log.info("Success! The document is verified");
                   }
   
                   byte[] alteredDigest = Verifier.flipRandomBit(digestBytes);
                   log.info(String.format("Flipping one bit in the digest and assert that the document is NOT verified. "
                           + "The altered digest is: %s", Verifier.toBase64(alteredDigest)));
                   verified = Verifier.verify(
                           document.getHash(),
                           alteredDigest,
                           proofResult.getProof().getIonText()
                   );
   
                   if (verified) {
                       throw new AssertionError("Expected document to not be verified against altered digest.");
                   } else {
                       log.info("Success! As expected flipping a bit in the digest causes verification to fail.");
                   }
   
                   byte[] alteredDocumentHash = Verifier.flipRandomBit(document.getHash());
                   log.info(String.format("Flipping one bit in the document's hash and assert that it is NOT verified. "
                           + "The altered document hash is: %s.", Verifier.toBase64(alteredDocumentHash)));
                   verified = Verifier.verify(
                           alteredDocumentHash,
                           digestBytes,
                           proofResult.getProof().getIonText()
                   );
   
                   if (verified) {
                       throw new AssertionError("Expected altered document hash to not be verified against digest.");
                   } else {
                       log.info("Success! As expected flipping a bit in the document hash causes verification to fail.");
                   }
               }
   
           } catch (Exception e) {
               log.error("Failed to verify digests.", e);
               throw e;
           }
   
           log.info(String.format("Finished verifying the registration with VIN=%s in ledger=%s.", vin, ledgerName));
       }
   
       /**
        * Get the revision of a particular document specified by the given document ID and block address.
        *
        * @param ledgerName
        *              Name of the ledger containing the document.
        * @param documentId
        *              Unique ID for the document to be verified, contained in the committed view of the document.
        * @param digestTipAddress
        *              The latest block location covered by the digest.
        * @param blockAddress
        *              The location of the block to request.
        * @return the requested revision.
        */
       public static GetRevisionResult getRevision(final String ledgerName, final String documentId,
                                                   final ValueHolder digestTipAddress, final BlockAddress blockAddress) {
           try {
               GetRevisionRequest request = new GetRevisionRequest()
                       .withName(ledgerName)
                       .withDigestTipAddress(digestTipAddress)
                       .withBlockAddress(new ValueHolder().withIonText(Constants.MAPPER.writeValueAsIonValue(blockAddress)
                               .toString()))
                       .withDocumentId(documentId);
               return client.getRevision(request);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Query the registration history for the given VIN.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              The unique VIN to query.
        * @return a list of {@link IonStruct} representing the registration history.
        * @throws IllegalStateException if failed to convert parameters into {@link IonValue}
        */
       public static List<IonStruct> queryRegistrationsByVin(final TransactionExecutor txn, final String vin) {
           log.info(String.format("Let's query the 'VehicleRegistration' table for VIN: %s...", vin));
           log.info("Let's query the 'VehicleRegistration' table for VIN: {}...", vin);
           final String query = String.format("SELECT * FROM _ql_committed_%s WHERE data.VIN = ?",
                   Constants.VEHICLE_REGISTRATION_TABLE_NAME);
           try {
               final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(vin));
               final Result result = txn.execute(query, parameters);
               List<IonStruct> list = ScanTable.toIonStructs(result);
               log.info(String.format("Found %d document(s)!", list.size()));
               return list;
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   }
   ```

------
#### [ 1.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazon.ion.IonReader;
   import com.amazon.ion.IonStruct;
   import com.amazon.ion.IonValue;
   import com.amazon.ion.IonWriter;
   import com.amazon.ion.system.IonReaderBuilder;
   import com.amazonaws.services.qldb.AmazonQLDB;
   import com.amazonaws.services.qldb.model.GetDigestResult;
   import com.amazonaws.services.qldb.model.GetRevisionRequest;
   import com.amazonaws.services.qldb.model.GetRevisionResult;
   import com.amazonaws.services.qldb.model.ValueHolder;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   import software.amazon.qldb.QldbSession;
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.SampleData;
   import software.amazon.qldb.tutorial.qldb.BlockAddress;
   import software.amazon.qldb.tutorial.qldb.QldbRevision;
   import software.amazon.qldb.tutorial.qldb.QldbStringUtils;
   
   import java.io.ByteArrayOutputStream;
   import java.io.IOException;
   import java.util.ArrayList;
   import java.util.Collections;
   import java.util.List;
   
   /**
    * Verify the integrity of a document revision in a QLDB ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class GetRevision {
       public static final Logger log = LoggerFactory.getLogger(GetRevision.class);
       public static AmazonQLDB client = CreateLedger.getClient();
   
       private GetRevision() { }
   
       public static void main(String... args) throws Exception {
   
           final String vin = SampleData.REGISTRATIONS.get(0).getVin();
   
           try (QldbSession qldbSession = ConnectToLedger.createQldbSession()) {
               verifyRegistration(qldbSession, Constants.LEDGER_NAME, vin);
           }
       }
   
       /**
        * Verify each version of the registration for the given VIN.
        *
        * @param qldbSession
        *              A QLDB session.
        * @param ledgerName
        *              The ledger to get digest from.
        * @param vin
        *              VIN to query the revision history of a specific registration with.
        * @throws Exception if failed to verify digests.
        * @throws AssertionError if document revision verification failed.
        */
       public static void verifyRegistration(final QldbSession qldbSession, final String ledgerName, final String vin)
               throws Exception {
           log.info(String.format("Let's verify the registration with VIN=%s, in ledger=%s.", vin, ledgerName));
   
           try {
               log.info("First, let's get a digest.");
               GetDigestResult digestResult = GetDigest.getDigest(ledgerName);
   
               ValueHolder digestTipAddress = digestResult.getDigestTipAddress();
               byte[] digestBytes = Verifier.convertByteBufferToByteArray(digestResult.getDigest());
   
               log.info("Got a ledger digest. Digest end address={}, digest={}.",
                   QldbStringUtils.toUnredactedString(digestTipAddress),
                   Verifier.toBase64(digestBytes));
   
               log.info(String.format("Next, let's query the registration with VIN=%s. "
                       + "Then we can verify each version of the registration.", vin));
               List<IonStruct> documentsWithMetadataList = new ArrayList<>();
               qldbSession.execute(txn -> {
                   documentsWithMetadataList.addAll(queryRegistrationsByVin(txn, vin));
               }, (retryAttempt) -> log.info("Retrying due to OCC conflict..."));
               log.info("Registrations queried successfully!");
   
               log.info(String.format("Found %s revisions of the registration with VIN=%s.",
                       documentsWithMetadataList.size(), vin));
   
               for (IonStruct ionStruct : documentsWithMetadataList) {
   
                   QldbRevision document = QldbRevision.fromIon(ionStruct);
                   log.info(String.format("Let's verify the document: %s", document));
   
                   log.info("Let's get a proof for the document.");
                   GetRevisionResult proofResult = getRevision(
                           ledgerName,
                           document.getMetadata().getId(),
                           digestTipAddress,
                           document.getBlockAddress()
                   );
   
                   final IonValue proof = Constants.MAPPER.writeValueAsIonValue(proofResult.getProof());
                   final IonReader reader = IonReaderBuilder.standard().build(proof);
                   reader.next();
                   ByteArrayOutputStream baos = new ByteArrayOutputStream();
                   IonWriter writer = Constants.SYSTEM.newBinaryWriter(baos);
                   writer.writeValue(reader);
                   writer.close();
                   baos.flush();
                   baos.close();
                   byte[] byteProof = baos.toByteArray();
   
                   log.info(String.format("Got back a proof: %s", Verifier.toBase64(byteProof)));
   
                   boolean verified = Verifier.verify(
                           document.getHash(),
                           digestBytes,
                           proofResult.getProof().getIonText()
                   );
   
                   if (!verified) {
                       throw new AssertionError("Document revision is not verified!");
                   } else {
                       log.info("Success! The document is verified");
                   }
   
                   byte[] alteredDigest = Verifier.flipRandomBit(digestBytes);
                   log.info(String.format("Flipping one bit in the digest and assert that the document is NOT verified. "
                           + "The altered digest is: %s", Verifier.toBase64(alteredDigest)));
                   verified = Verifier.verify(
                           document.getHash(),
                           alteredDigest,
                           proofResult.getProof().getIonText()
                   );
   
                   if (verified) {
                       throw new AssertionError("Expected document to not be verified against altered digest.");
                   } else {
                       log.info("Success! As expected flipping a bit in the digest causes verification to fail.");
                   }
   
                   byte[] alteredDocumentHash = Verifier.flipRandomBit(document.getHash());
                   log.info(String.format("Flipping one bit in the document's hash and assert that it is NOT verified. "
                           + "The altered document hash is: %s.", Verifier.toBase64(alteredDocumentHash)));
                   verified = Verifier.verify(
                           alteredDocumentHash,
                           digestBytes,
                           proofResult.getProof().getIonText()
                   );
   
                   if (verified) {
                       throw new AssertionError("Expected altered document hash to not be verified against digest.");
                   } else {
                       log.info("Success! As expected flipping a bit in the document hash causes verification to fail.");
                   }
               }
   
           } catch (Exception e) {
               log.error("Failed to verify digests.", e);
               throw e;
           }
   
           log.info(String.format("Finished verifying the registration with VIN=%s in ledger=%s.", vin, ledgerName));
       }
   
       /**
        * Get the revision of a particular document specified by the given document ID and block address.
        *
        * @param ledgerName
        *              Name of the ledger containing the document.
        * @param documentId
        *              Unique ID for the document to be verified, contained in the committed view of the document.
        * @param digestTipAddress
        *              The latest block location covered by the digest.
        * @param blockAddress
        *              The location of the block to request.
        * @return the requested revision.
        */
       public static GetRevisionResult getRevision(final String ledgerName, final String documentId,
                                                   final ValueHolder digestTipAddress, final BlockAddress blockAddress) {
           try {
               GetRevisionRequest request = new GetRevisionRequest()
                       .withName(ledgerName)
                       .withDigestTipAddress(digestTipAddress)
                       .withBlockAddress(new ValueHolder().withIonText(Constants.MAPPER.writeValueAsIonValue(blockAddress)
                               .toString()))
                       .withDocumentId(documentId);
               return client.getRevision(request);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Query the registration history for the given VIN.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              The unique VIN to query.
        * @return a list of {@link IonStruct} representing the registration history.
        * @throws IllegalStateException if failed to convert parameters into {@link IonValue}
        */
       public static List<IonStruct> queryRegistrationsByVin(final TransactionExecutor txn, final String vin) {
           log.info(String.format("Let's query the 'VehicleRegistration' table for VIN: %s...", vin));
           log.info("Let's query the 'VehicleRegistration' table for VIN: {}...", vin);
           final String query = String.format("SELECT * FROM _ql_committed_%s WHERE data.VIN = ?",
                   Constants.VEHICLE_REGISTRATION_TABLE_NAME);
           try {
               final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(vin));
               final Result result = txn.execute(query, parameters);
               List<IonStruct> list = ScanTable.toIonStructs(result);
               log.info(String.format("Found %d document(s)!", list.size()));
               return list;
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   }
   ```

------
**注意**  
在`getRevision`方法返回指定文档修订版本的证明后，此程序使用客户端 API 验证该修订版。有关此 API 使用的算法的概述，请参阅 [使用证明重新计算您的摘要](verification.results.md#verification.results.recalc)。

1. 编译并运行`GetRevision.java`程序，使用 VIN `1N4AL11D75C109151` 对 `VehicleRegistration` 文档进行加密验证。

要导出和验证 `vehicle-registration` 分类账中的日记账数据，请继续 [步骤 8：导出并验证分类账中的日记账数据](getting-started.java.step-8.md)。

# 步骤 8：导出并验证分类账中的日记账数据
<a name="getting-started.java.step-8"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在 Amazon QLDB 中，您可出于各种目的访问分类账中的日记账内容，例如数据保留、分析和审计。有关更多信息，请参阅 [从Amazon QLDB 导出日记账数据](export-journal.md)。

在此步骤中，您将`vehicle-registration`分类账中的[日记账数据块](journal-contents.md)导出至 Amazon S3 存储桶中。然后，您可使用导出的数据验证日记账数据块和每个区块中各个哈希组件之间的哈希链。

您使用的 AWS Identity and Access Management (IAM) 委托人实体必须具有足够的 IAM 权限才能在您的中创建 Amazon S3 存储桶 AWS 账户。有关更多信息，请参阅 *Amazon S3 用户指南* 中的 [Policies and Permissions in Amazon S3 中的策略和权限](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-policy-language-overview.html)。您还必须有权创建附带权限策略的 IAM 角色，该策略允许 QLDB 将对象写入至您的 Amazon S3 存储桶。有关更多信息，请参阅 *IAM 用户指南*中的[访问 IAM 资源所需的权限](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_permissions-required.html)。

**导出和验证日记账数据**

1. 查看以下文件（`JournalBlock.java`），该文件代表日记账数据块及其数据内容。它包括一个名为`verifyBlockHash()`的方法，该方法演示了如何计算区块哈希的每个组成部分。

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial.qldb;
   
   import com.fasterxml.jackson.annotation.JsonCreator;
   import com.fasterxml.jackson.annotation.JsonProperty;
   import com.fasterxml.jackson.databind.annotation.JsonSerialize;
   import com.fasterxml.jackson.dataformat.ion.IonTimestampSerializers;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   import software.amazon.qldb.tutorial.Constants;
   import software.amazon.qldb.tutorial.Verifier;
   
   import java.io.IOException;
   import java.nio.ByteBuffer;
   import java.util.Arrays;
   import java.util.Date;
   import java.util.HashSet;
   import java.util.List;
   import java.util.Set;
   import java.util.stream.Collectors;
   
   import static java.nio.ByteBuffer.wrap;
   
   /**
    * Represents a JournalBlock that was recorded after executing a transaction
    * in the ledger.
    */
   public final class JournalBlock {
       private static final Logger log = LoggerFactory.getLogger(JournalBlock.class);
   
       private BlockAddress blockAddress;
       private String transactionId;
       @JsonSerialize(using = IonTimestampSerializers.IonTimestampJavaDateSerializer.class)
       private Date blockTimestamp;
       private byte[] blockHash;
       private byte[] entriesHash;
       private byte[] previousBlockHash;
       private byte[][] entriesHashList;
       private TransactionInfo transactionInfo;
       private RedactionInfo redactionInfo;
       private List<QldbRevision> revisions;
   
       @JsonCreator
       public JournalBlock(@JsonProperty("blockAddress") final BlockAddress blockAddress,
                           @JsonProperty("transactionId") final String transactionId,
                           @JsonProperty("blockTimestamp") final Date blockTimestamp,
                           @JsonProperty("blockHash") final byte[] blockHash,
                           @JsonProperty("entriesHash") final byte[] entriesHash,
                           @JsonProperty("previousBlockHash") final byte[] previousBlockHash,
                           @JsonProperty("entriesHashList") final byte[][] entriesHashList,
                           @JsonProperty("transactionInfo") final TransactionInfo transactionInfo,
                           @JsonProperty("redactionInfo") final RedactionInfo redactionInfo,
                           @JsonProperty("revisions") final List<QldbRevision> revisions) {
           this.blockAddress = blockAddress;
           this.transactionId = transactionId;
           this.blockTimestamp = blockTimestamp;
           this.blockHash = blockHash;
           this.entriesHash = entriesHash;
           this.previousBlockHash = previousBlockHash;
           this.entriesHashList = entriesHashList;
           this.transactionInfo = transactionInfo;
           this.redactionInfo = redactionInfo;
           this.revisions = revisions;
       }
   
       public BlockAddress getBlockAddress() {
           return blockAddress;
       }
   
       public String getTransactionId() {
           return transactionId;
       }
   
       public Date getBlockTimestamp() {
           return blockTimestamp;
       }
   
       public byte[][] getEntriesHashList() {
           return entriesHashList;
       }
   
       public TransactionInfo getTransactionInfo() {
           return transactionInfo;
       }
   
       public RedactionInfo getRedactionInfo() {
           return redactionInfo;
       }
   
       public List<QldbRevision> getRevisions() {
           return revisions;
       }
   
       public byte[] getEntriesHash() {
           return entriesHash;
       }
   
       public byte[] getBlockHash() {
           return blockHash;
       }
   
       public byte[] getPreviousBlockHash() {
           return previousBlockHash;
       }
   
       @Override
       public String toString() {
           return "JournalBlock{"
                   + "blockAddress=" + blockAddress
                   + ", transactionId='" + transactionId + '\''
                   + ", blockTimestamp=" + blockTimestamp
                   + ", blockHash=" + Arrays.toString(blockHash)
                   + ", entriesHash=" + Arrays.toString(entriesHash)
                   + ", previousBlockHash=" + Arrays.toString(previousBlockHash)
                   + ", entriesHashList=" + Arrays.toString(entriesHashList)
                   + ", transactionInfo=" + transactionInfo
                   + ", redactionInfo=" + redactionInfo
                   + ", revisions=" + revisions
                   + '}';
       }
   
       @Override
       public boolean equals(final Object o) {
           if (this == o) {
               return true;
           }
           if (!(o instanceof JournalBlock)) {
               return false;
           }
   
           final JournalBlock that = (JournalBlock) o;
   
           if (!getBlockAddress().equals(that.getBlockAddress())) {
               return false;
           }
           if (!getTransactionId().equals(that.getTransactionId())) {
               return false;
           }
           if (!getBlockTimestamp().equals(that.getBlockTimestamp())) {
               return false;
           }
           if (!Arrays.equals(getBlockHash(), that.getBlockHash())) {
               return false;
           }
           if (!Arrays.equals(getEntriesHash(), that.getEntriesHash())) {
               return false;
           }
           if (!Arrays.equals(getPreviousBlockHash(), that.getPreviousBlockHash())) {
               return false;
           }
           if (!Arrays.deepEquals(getEntriesHashList(), that.getEntriesHashList())) {
               return false;
           }
           if (!getTransactionInfo().equals(that.getTransactionInfo())) {
               return false;
           }
           if (getRedactionInfo() != null ? !getRedactionInfo().equals(that.getRedactionInfo()) : that.getRedactionInfo() != null) {
               return false;
           }
           return getRevisions() != null ? getRevisions().equals(that.getRevisions()) : that.getRevisions() == null;
       }
   
       @Override
       public int hashCode() {
           int result = getBlockAddress().hashCode();
           result = 31 * result + getTransactionId().hashCode();
           result = 31 * result + getBlockTimestamp().hashCode();
           result = 31 * result + Arrays.hashCode(getBlockHash());
           result = 31 * result + Arrays.hashCode(getEntriesHash());
           result = 31 * result + Arrays.hashCode(getPreviousBlockHash());
           result = 31 * result + Arrays.deepHashCode(getEntriesHashList());
           result = 31 * result + getTransactionInfo().hashCode();
           result = 31 * result + (getRedactionInfo() != null ? getRedactionInfo().hashCode() : 0);
           result = 31 * result + (getRevisions() != null ? getRevisions().hashCode() : 0);
           return result;
       }
   
       /**
        * This method validates that the hashes of the components of a journal block make up the block
        * hash that is provided with the block itself.
        *
        * The components that contribute to the hash of the journal block consist of the following:
        *   - user transaction information (contained in [transactionInfo])
        *   - user redaction information (contained in [redactionInfo])
        *   - user revisions (contained in [revisions])
        *   - hashes of internal-only system metadata (contained in [revisions] and in [entriesHashList])
        *   - the previous block hash
        *
        * If any of the computed hashes of user information cannot be validated or any of the system
        * hashes do not result in the correct computed values, this method will throw an IllegalArgumentException.
        *
        * Internal-only system metadata is represented by its hash, and can be present in the form of certain
        * items in the [revisions] list that only contain a hash and no user data, as well as some hashes
        * in [entriesHashList].
        *
        * To validate that the hashes of the user data are valid components of the [blockHash], this method
        * performs the following steps:
        *
        * 1. Compute the hash of the [transactionInfo] and validate that it is included in the [entriesHashList].
        * 2. Compute the hash of the [redactionInfo], if present, and validate that it is included in the [entriesHashList].
        * 3. Validate the hash of each user revision was correctly computed and matches the hash published
        * with that revision.
        * 4. Compute the hash of the [revisions] by treating the revision hashes as the leaf nodes of a Merkle tree
        * and calculating the root hash of that tree. Then validate that hash is included in the [entriesHashList].
        * 5. Compute the hash of the [entriesHashList] by treating the hashes as the leaf nodes of a Merkle tree
        * and calculating the root hash of that tree. Then validate that hash matches [entriesHash].
        * 6. Finally, compute the block hash by computing the hash resulting from concatenating the [entriesHash]
        * and previous block hash, and validate that the result matches the [blockHash] provided by QLDB with the block.
        *
        * This method is called by ValidateQldbHashChain::verify for each journal block to validate its
        * contents before verifying that the hash chain between consecutive blocks is correct.
        */
       public void verifyBlockHash() {
           Set<ByteBuffer> entriesHashSet = new HashSet<>();
           Arrays.stream(entriesHashList).forEach(hash -> entriesHashSet.add(wrap(hash).asReadOnlyBuffer()));
   
           byte[] computedTransactionInfoHash = computeTransactionInfoHash();
           if (!entriesHashSet.contains(wrap(computedTransactionInfoHash).asReadOnlyBuffer())) {
               throw new IllegalArgumentException(
                       "Block transactionInfo hash is not contained in the QLDB block entries hash list.");
           }
   
           if (redactionInfo != null) {
               byte[] computedRedactionInfoHash = computeRedactionInfoHash();
               if (!entriesHashSet.contains(wrap(computedRedactionInfoHash).asReadOnlyBuffer())) {
                   throw new IllegalArgumentException(
                           "Block redactionInfo hash is not contained in the QLDB block entries hash list.");
               }
           }
   
           if (revisions != null) {
               revisions.forEach(QldbRevision::verifyRevisionHash);
               byte[] computedRevisionsHash = computeRevisionsHash();
               if (!entriesHashSet.contains(wrap(computedRevisionsHash).asReadOnlyBuffer())) {
                   throw new IllegalArgumentException(
                           "Block revisions list hash is not contained in the QLDB block entries hash list.");
               }
           }
   
           byte[] computedEntriesHash = computeEntriesHash();
           if (!Arrays.equals(computedEntriesHash, entriesHash)) {
               throw new IllegalArgumentException("Computed entries hash does not match entries hash provided in the block.");
           }
   
           byte[] computedBlockHash = Verifier.dot(computedEntriesHash, previousBlockHash);
           if (!Arrays.equals(computedBlockHash, blockHash)) {
               throw new IllegalArgumentException("Computed block hash does not match block hash provided in the block.");
           }
       }
   
       private byte[] computeTransactionInfoHash() {
           try {
               return QldbIonUtils.hashIonValue(Constants.MAPPER.writeValueAsIonValue(transactionInfo));
           } catch (IOException e) {
               throw new IllegalArgumentException("Could not compute transactionInfo hash to verify block hash.", e);
           }
       }
   
       private byte[] computeRedactionInfoHash() {
           try {
               return QldbIonUtils.hashIonValue(Constants.MAPPER.writeValueAsIonValue(redactionInfo));
           } catch (IOException e) {
               throw new IllegalArgumentException("Could not compute redactionInfo hash to verify block hash.", e);
           }
       }
   
       private byte[] computeRevisionsHash() {
           return Verifier.calculateMerkleTreeRootHash(revisions.stream().map(QldbRevision::getHash).collect(Collectors.toList()));
       }
   
       private byte[] computeEntriesHash() {
           return Verifier.calculateMerkleTreeRootHash(Arrays.asList(entriesHashList));
       }
   }
   ```

1. 编译并运行以下程序（`ValidateQldbHashChain.java`），以执行以下步骤：

   1. 将账本中的日记`vehicle-registration`账块导出到名为**qldb-tutorial-journal-export-*111122223333***（用您的 AWS 账户 数字替换）的 Amazon S3 存储桶中。

   1. 通过调用`verifyBlockHash()`来验证每个区块中的各个哈希组件。

   1. 验证日记账数据块之间的哈希链。

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazonaws.services.qldb.model.ExportJournalToS3Result;
   import com.amazonaws.services.qldb.model.S3EncryptionConfiguration;
   import com.amazonaws.services.qldb.model.S3ObjectEncryptionType;
   import com.amazonaws.services.s3.AmazonS3ClientBuilder;
   
   import java.time.Instant;
   import java.util.Arrays;
   import java.util.List;
   
   import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder;
   import com.amazonaws.services.securitytoken.model.GetCallerIdentityRequest;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import software.amazon.qldb.tutorial.qldb.JournalBlock;
   
   /**
    * Validate the hash chain of a QLDB ledger by stepping through its S3 export.
    *
    * This code accepts an exportId as an argument, if exportId is passed the code
    * will use that or request QLDB to generate a new export to perform QLDB hash
    * chain validation.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class ValidateQldbHashChain {
       public static final Logger log = LoggerFactory.getLogger(ValidateQldbHashChain.class);
       private static final int TIME_SKEW = 20;
   
       private ValidateQldbHashChain() { }
   
       /**
        * Export journal contents to a S3 bucket.
        *
        * @return the ExportId of the journal export.
        * @throws InterruptedException if the thread is interrupted while waiting for export to complete.
        */
       private static String createExport() throws InterruptedException {
           String accountId = AWSSecurityTokenServiceClientBuilder.defaultClient()
               .getCallerIdentity(new GetCallerIdentityRequest()).getAccount();
           String bucketName = Constants.JOURNAL_EXPORT_S3_BUCKET_NAME_PREFIX + "-" + accountId;
           String prefix = Constants.LEDGER_NAME + "-" + Instant.now().getEpochSecond() + "/";
   
           S3EncryptionConfiguration encryptionConfiguration = new S3EncryptionConfiguration()
                   .withObjectEncryptionType(S3ObjectEncryptionType.SSE_S3);
           ExportJournalToS3Result exportJournalToS3Result = 
               ExportJournal.createJournalExportAndAwaitCompletion(Constants.LEDGER_NAME, 
                       bucketName, prefix, null, encryptionConfiguration, ExportJournal.DEFAULT_EXPORT_TIMEOUT_MS);
   
           return exportJournalToS3Result.getExportId();
       }
   
       /**
        * Validates that the chain hash on the {@link JournalBlock} is valid.
        *
        * @param journalBlocks
        *              {@link JournalBlock} containing hashes to validate.
        * @throws IllegalStateException if previous block hash does not match.
        */
       public static void verify(final List<JournalBlock> journalBlocks) {
           if (journalBlocks.size() == 0) {
               return;
           }
   
           journalBlocks.stream().reduce(null, (previousJournalBlock, journalBlock) -> {
               journalBlock.verifyBlockHash();
               if (previousJournalBlock == null) { return journalBlock; }
               if (!Arrays.equals(previousJournalBlock.getBlockHash(), journalBlock.getPreviousBlockHash())) {
                   throw new IllegalStateException("Previous block hash doesn't match.");
               }
               byte[] blockHash = Verifier.dot(journalBlock.getEntriesHash(), previousJournalBlock.getBlockHash());
               if (!Arrays.equals(blockHash, journalBlock.getBlockHash())) {
                   throw new IllegalStateException("Block hash doesn't match entriesHash dot previousBlockHash, the chain is "
                           + "broken.");
               }
               return journalBlock;
           });
       }
   
       public static void main(final String... args) throws InterruptedException {
           try {
               String exportId;
               if (args.length == 1) {
                   exportId = args[0];
                   log.info("Validating QLDB hash chain for exportId: " + exportId);
               } else {
                   log.info("Requesting QLDB to create an export.");
                   exportId = createExport();
               }
               List<JournalBlock> journalBlocks =
                   JournalS3ExportReader.readExport(DescribeJournalExport.describeExport(Constants.LEDGER_NAME,
                       exportId), AmazonS3ClientBuilder.defaultClient());
               verify(journalBlocks);
           } catch (Exception e) {
               log.error("Unable to perform hash chain verification.", e);
               throw e;
           }
       }
   
   }
   ```

如果您不再需要使用`vehicle-registration`分类账，请继续[步骤 9：（可选）清除资源](getting-started.java.step-9.md)。

# 步骤 9：（可选）清除资源
<a name="getting-started.java.step-9"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

您可以继续使用 `vehicle-registration` 分类账。但是，如果您不再需要，请将其删除。

**删除分类账**

1. 编译并运行以下程序（`DeleteLedger.java`）以删除您的 `vehicle-registration` 分类账及其所有内容。

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazonaws.services.qldb.AmazonQLDB;
   import com.amazonaws.services.qldb.model.DeleteLedgerRequest;
   import com.amazonaws.services.qldb.model.DeleteLedgerResult;
   import com.amazonaws.services.qldb.model.ResourceNotFoundException;
   import com.amazonaws.services.qldb.model.UpdateLedgerRequest;
   import com.amazonaws.services.qldb.model.UpdateLedgerResult;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   /**
    * Delete a ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class DeleteLedger {
       public static final Logger log = LoggerFactory.getLogger(DeleteLedger.class);
       public static final Long LEDGER_DELETION_POLL_PERIOD_MS = 20_000L;
       public static AmazonQLDB client = CreateLedger.getClient();
   
       private DeleteLedger() { }
   
       public static void main(String... args) throws Exception {
           try {
               setDeletionProtection(Constants.LEDGER_NAME, false);
   
               delete(Constants.LEDGER_NAME);
   
               waitForDeleted(Constants.LEDGER_NAME);
   
           } catch (Exception e) {
               log.error("Unable to delete the ledger.", e);
               throw e;
           }
       }
   
       /**
        * Send a request to the QLDB database to delete the specified ledger.
        *
        * @param ledgerName
        *              Name of the ledger to be deleted.
        * @return DeleteLedgerResult.
        */
       public static DeleteLedgerResult delete(final String ledgerName) {
           log.info("Attempting to delete the ledger with name: {}...", ledgerName);
           DeleteLedgerRequest request = new DeleteLedgerRequest().withName(ledgerName);
           DeleteLedgerResult result = client.deleteLedger(request);
           log.info("Success.");
           return result;
       }
   
       /**
        * Wait for the ledger to be deleted.
        *
        * @param ledgerName
        *              Name of the ledger being deleted.
        * @throws InterruptedException if thread is being interrupted.
        */
       public static void waitForDeleted(final String ledgerName) throws InterruptedException {
           log.info("Waiting for the ledger to be deleted...");
           while (true) {
               try {
                   DescribeLedger.describe(ledgerName);
                   log.info("The ledger is still being deleted. Please wait...");
                   Thread.sleep(LEDGER_DELETION_POLL_PERIOD_MS);
               } catch (ResourceNotFoundException ex) {
                   log.info("Success. The ledger is deleted.");
                   break;
               }
           }
       }
       
       public static UpdateLedgerResult setDeletionProtection(String ledgerName, boolean deletionProtection) {
           log.info("Let's set deletionProtection to {} for the ledger with name {}", deletionProtection, ledgerName);
           UpdateLedgerRequest request = new UpdateLedgerRequest()
                   .withName(ledgerName)
                   .withDeletionProtection(deletionProtection);
   
           UpdateLedgerResult result = client.updateLedger(request);
           log.info("Success. Ledger updated: {}", result);
           return result;
       }
   }
   ```
**注意**  
如果您的分类账启用了删除保护，您必须先禁用它，然后才能使用 QLDB API 删除分类账。

1. 如果您在[上一步](getting-started.java.step-8.md)中导出了日记账数据，但不再需要这些数据，请使用 Amazon S3 控制台删除 S3 存储桶。

   打开 Amazon S3 控制台，网址为 [https://console.aws.amazon.com/s3/](https://console.aws.amazon.com/s3/)。

# Amazon QLDB Node.js 教程
<a name="getting-started.nodejs.tutorial"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在本教程示例应用程序的本实现中，您将使用 Amazon QLDB 驱动程序和 Node.js 中的软件开发工具包来创建 QLD JavaScript B 账本并使用示例数据填充 AWS 该账本。

在学习本教程时，您可以参考 [适用于 JavaScript 的 AWS SDK API 参考](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/) 以了解管理 API 操作。有关事务数据操作，您可参见[适用于 Node.js API 参考的 QLDB 驱动程序](https://amazon-qldb-docs.s3.amazonaws.com/drivers/nodejs/3.1.0/index.html)。

**注意**  
在适用的情况下，某些用例对于 Node.js 的 QLDB 驱动程序的每个支持主要版本都配备不同的命令或代码示例。

**Topics**
+ [

# 安装 Amazon QLDB Node.js 示例应用程序
](sample-app.nodejs.md)
+ [

# 第 1 步：创建新的分类账
](getting-started.nodejs.step-1.md)
+ [

# 第 2 步：测试与分类账的连接
](getting-started.nodejs.step-2.md)
+ [

# 步骤 3：创建表、索引与示例数据
](getting-started.nodejs.step-3.md)
+ [

# 步骤 4：查询分类账中的表
](getting-started.nodejs.step-4.md)
+ [

# 第 5 步：修改分类账中的文档
](getting-started.nodejs.step-5.md)
+ [

# 步骤 6：查看文档修订历史记录
](getting-started.nodejs.step-6.md)
+ [

# 第 7 步：验证分类账中的文档
](getting-started.nodejs.step-7.md)
+ [

# 第 8 步（可选）：清除资源
](getting-started.nodejs.step-8.md)

# 安装 Amazon QLDB Node.js 示例应用程序
<a name="sample-app.nodejs"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

本节介绍如何安装和运行为 Node.js 教程提供的亚马逊 QLDB 示例应用程序。 step-by-step此示例应用程序的用例是机动车辆部门（DMV）数据库，用于追踪有关车辆登记的完整历史信息。

Node.js 的 DMV 示例应用程序在 GitHub 存储库 [aws-samples/-amazon-qldb-dmv-sample](https://github.com/aws-samples/amazon-qldb-dmv-sample-nodejs) nodejs 中是开源的。

## 先决条件
<a name="sample-app.nodejs.prereqs"></a>

在开始之前，请确保您已完成适用于 Node.js [先决条件](getting-started.nodejs.md#getting-started.nodejs.prereqs) 的 QLDB 驱动程序。这包括安装 Node.js 并执行以下操作：

1. 报名参加 AWS.

1. 创建具有适当 QLDB 权限的用户。

1. 授权以编程方式访问开发。

要完成本教程中的所有步骤，您需要通过 QLDB API 对分类账资源拥有完全管理权限。

## 安装
<a name="sample-app.nodejs.install"></a>

**要安装示例应用程序**

1. 输入以下命令以从中克隆示例应用程序 GitHub。

------
#### [ 2.x ]

   ```
   git clone https://github.com/aws-samples/amazon-qldb-dmv-sample-nodejs.git
   ```

------
#### [ 1.x ]

   ```
   git clone -b v1.0.0 https://github.com/aws-samples/amazon-qldb-dmv-sample-nodejs.git
   ```

------

   该示例应用程序打包了本教程中的完整源代码及其依赖项，包括 Node.js 驱动程序和 [Node.js JavaScript 中的 S AWS DK](https://aws.amazon.com/sdk-for-node-js)。此应用程序是用编写的 TypeScript。

1. 切换到克隆 `amazon-qldb-dmv-sample-nodejs` 包的目录。

   ```
   cd amazon-qldb-dmv-sample-nodejs
   ```

1. 对依赖项进行全新安装。

   ```
   npm ci
   ```

1. 转换包。

   ```
   npm run build
   ```

   转译后的 JavaScript 文件写入`./dist`目录中。

1. 继续 [第 1 步：创建新的分类账](getting-started.nodejs.step-1.md) 开始教程并创建分类账。

# 第 1 步：创建新的分类账
<a name="getting-started.nodejs.step-1"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在此步骤中，您将创建一个名为 `vehicle-registration` 的新 Amazon QLDB 分类账。

**创建新分类账**

1. 查看以下文件（`Constants.ts`），其包含本教程中所有其他程序使用的常量值。

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   /**
    * Constant values used throughout this tutorial.
    */
   export const LEDGER_NAME = "vehicle-registration";
   export const LEDGER_NAME_WITH_TAGS = "tags";
   
   export const DRIVERS_LICENSE_TABLE_NAME = "DriversLicense";
   export const PERSON_TABLE_NAME = "Person";
   export const VEHICLE_REGISTRATION_TABLE_NAME = "VehicleRegistration";
   export const VEHICLE_TABLE_NAME = "Vehicle";
   
   export const GOV_ID_INDEX_NAME = "GovId";
   export const LICENSE_NUMBER_INDEX_NAME = "LicenseNumber";
   export const LICENSE_PLATE_NUMBER_INDEX_NAME = "LicensePlateNumber";
   export const PERSON_ID_INDEX_NAME = "PersonId";
   export const VIN_INDEX_NAME = "VIN";
   
   export const RETRY_LIMIT = 4;
   
   export const JOURNAL_EXPORT_S3_BUCKET_NAME_PREFIX = "qldb-tutorial-journal-export";
   export const USER_TABLES = "information_schema.user_tables";
   ```

1. 使用以下程序（`CreateLedger.ts`）创建名为 `vehicle-registration` 的分类账。

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QLDB } from "aws-sdk";
   import {
       CreateLedgerRequest,
       CreateLedgerResponse,
       DescribeLedgerRequest,
       DescribeLedgerResponse
   } from "aws-sdk/clients/qldb";
   
   import { LEDGER_NAME } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   import { sleep } from "./qldb/Util";
   
   const LEDGER_CREATION_POLL_PERIOD_MS = 10000;
   const ACTIVE_STATE = "ACTIVE";
   
   /**
    * Create a new ledger with the specified name.
    * @param ledgerName Name of the ledger to be created.
    * @param qldbClient The QLDB control plane client to use.
    * @returns Promise which fulfills with a CreateLedgerResponse.
    */
   export async function createLedger(ledgerName: string, qldbClient: QLDB): Promise<CreateLedgerResponse> {
       log(`Creating a ledger named: ${ledgerName}...`);
       const request: CreateLedgerRequest = {
           Name: ledgerName,
           PermissionsMode: "ALLOW_ALL"
       }
       const result: CreateLedgerResponse = await qldbClient.createLedger(request).promise();
       log(`Success. Ledger state: ${result.State}.`);
       return result;
   }
   
   /**
    * Wait for the newly created ledger to become active.
    * @param ledgerName Name of the ledger to be checked on.
    * @param qldbClient The QLDB control plane client to use.
    * @returns Promise which fulfills with a DescribeLedgerResponse.
    */
   export async function waitForActive(ledgerName: string, qldbClient: QLDB): Promise<DescribeLedgerResponse> {
       log(`Waiting for ledger ${ledgerName} to become active...`);
       const request: DescribeLedgerRequest = {
           Name: ledgerName
       }
       while (true) {
           const result: DescribeLedgerResponse = await qldbClient.describeLedger(request).promise();
           if (result.State === ACTIVE_STATE) {
               log("Success. Ledger is active and ready to be used.");
               return result;
           }
           log("The ledger is still creating. Please wait...");
           await sleep(LEDGER_CREATION_POLL_PERIOD_MS);
       }
   }
   
   /**
    * Create a ledger and wait for it to be active.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbClient: QLDB = new QLDB();
           await createLedger(LEDGER_NAME, qldbClient);
           await waitForActive(LEDGER_NAME, qldbClient);
       } catch (e) {
           error(`Unable to create the ledger: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```
**注意**  
在 `createLedger` 调用中，您必须指定分类账名称和权限模式。我们强烈建议使用 `STANDARD` 权限模式来最大限度地提高分类账数据的安全性。
创建分类账时，将默认启用 *删除保护*。QLDB 中有一项功能，可防止分类账被任何用户删除。您可以选择使用 QLDB API 或 AWS Command Line Interface () 在创建账本时禁用删除保护。AWS CLI
您还可选择指定要附加到分类账的标签。

1. 要运行编译后的程序，请输入以下命令。

   ```
   node dist/CreateLedger.js
   ```

要验证您与新分类账的连接，请继续 [第 2 步：测试与分类账的连接](getting-started.nodejs.step-2.md)。

# 第 2 步：测试与分类账的连接
<a name="getting-started.nodejs.step-2"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在此步骤中，您将验证是否可以使用事务数据 API 端点连接至 Amazon QLDB 中的`vehicle-registration`分类账。

**测试与分类账的连接**

1. 使用以下程序（`ConnectToLedger.ts`），该程序创建了与`vehicle-registration` 分类账的数据会话连接。

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QldbDriver, RetryConfig  } from "amazon-qldb-driver-nodejs";
   import { ClientConfiguration } from "aws-sdk/clients/qldbsession";
   
   import { LEDGER_NAME } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   
   const qldbDriver: QldbDriver = createQldbDriver();
   
   /**
    * Create a driver for creating sessions.
    * @param ledgerName The name of the ledger to create the driver on.
    * @param serviceConfigurationOptions The configurations for the AWS SDK client that the driver uses.
    * @returns The driver for creating sessions.
    */
   export function createQldbDriver(
       ledgerName: string = LEDGER_NAME,
       serviceConfigurationOptions: ClientConfiguration = {}
   ): QldbDriver {
       const retryLimit = 4;
       const maxConcurrentTransactions = 10;
       //Use driver's default backoff function (and hence, no second parameter provided to RetryConfig)
       const retryConfig: RetryConfig = new RetryConfig(retryLimit);
       const qldbDriver: QldbDriver = new QldbDriver(ledgerName, serviceConfigurationOptions, maxConcurrentTransactions, retryConfig);
       return qldbDriver;
   }
   
   export function getQldbDriver(): QldbDriver {
       return qldbDriver;
   }
   
   /**
    * Connect to a session for a given ledger using default settings.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           log("Listing table names...");
           const tableNames: string[] = await qldbDriver.getTableNames();
           tableNames.forEach((tableName: string): void => {
               log(tableName);
           });
       } catch (e) {
           error(`Unable to create session: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```

------
#### [ 1.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QldbDriver  } from "amazon-qldb-driver-nodejs";
   import { ClientConfiguration } from "aws-sdk/clients/qldbsession";
   
   import { LEDGER_NAME } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   
   const qldbDriver: QldbDriver = createQldbDriver();
   
   /**
    * Create a driver for creating sessions.
    * @param ledgerName The name of the ledger to create the driver on.
    * @param serviceConfigurationOptions The configurations for the AWS SDK client that the driver uses.
    * @returns The driver for creating sessions.
    */
   export function createQldbDriver(
       ledgerName: string = LEDGER_NAME,
       serviceConfigurationOptions: ClientConfiguration = {}
   ): QldbDriver {
       const qldbDriver: QldbDriver = new QldbDriver(ledgerName, serviceConfigurationOptions);
       return qldbDriver;
   }
   
   export function getQldbDriver(): QldbDriver {
       return qldbDriver;
   }
   
   /**
    * Connect to a session for a given ledger using default settings.
    * @returns Promise which fulfills with void.
    */
   var main = async function(): Promise<void> {
       try {
           log("Listing table names...");
           const tableNames: string[] = await qldbDriver.getTableNames();
           tableNames.forEach((tableName: string): void => {
               log(tableName);
           });
       } catch (e) {
           error(`Unable to create session: ${e}`);
       } 
   }
   
   if (require.main === module) {
       main();
   }
   ```

------
**注意**  
要在分类账上运行数据事务，必须创建一个 QLDB 驱动对象以连接到指定的分类账。这与您在 [上一步](getting-started.nodejs.step-1.md) 中创建分类账时使用的 `qldbClient` 客户端对象不同。之前的客户端仅用于运行[Amazon QLDB API 参考](api-reference.md)中列出的管理 API 操作。

1. 要运行编译后的程序，请输入以下命令。

   ```
   node dist/ConnectToLedger.js
   ```

要在 `vehicle-registration` 分类账中创建表，请继续 [步骤 3：创建表、索引与示例数据](getting-started.nodejs.step-3.md)。

# 步骤 3：创建表、索引与示例数据
<a name="getting-started.nodejs.step-3"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

当您的 Amazon QLDB 分类账处于活动状态且接受连接时，您可开始创建有关车辆、车主和注册信息的数据表。创建表和索引后，可以向其中加载数据。

在此步骤中，您将在`vehicle-registration`分类账中创建四个表格：
+ `VehicleRegistration`
+ `Vehicle`
+ `Person`
+ `DriversLicense`

您还可创建以下索引。


****  

| 表名称 | 字段 | 
| --- | --- | 
| VehicleRegistration | VIN | 
| VehicleRegistration | LicensePlateNumber | 
| Vehicle | VIN | 
| Person | GovId | 
| DriversLicense | LicenseNumber | 
| DriversLicense | PersonId | 

插入示例数据时，首先要在 `Person` 表格中插入文档。然后使用系统分配的、来自每个`Person`文档的`id`填充适当`VehicleRegistration` 和 `DriversLicense`文档中的相应文档。

**提示**  
最佳做法是使用系统分配的 `id` 文档作为外键。虽然您可以定义作为唯一标识符的字段（例如车辆的 VIN），但文档的真正唯一标识符是`id`。字段都包含在文档元数据中，您可以在*提交视图*（系统定义的表格视图）中对其进行查询。  
有关 QLDB 中的视图的更多信息，请参阅[核心概念](ledger-structure.md)。了解有关元数据的更多信息，请参阅 [查询文档元数据](working.metadata.md)。

**创建表和索引**

1. 使用以下程序（`CreateTable.ts`）以创建前面提到的表。

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   
   import { getQldbDriver } from "./ConnectToLedger";
   import {
       DRIVERS_LICENSE_TABLE_NAME,
       PERSON_TABLE_NAME,
       VEHICLE_REGISTRATION_TABLE_NAME,
       VEHICLE_TABLE_NAME
   } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   
   /**
    * Create multiple tables in a single transaction.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param tableName Name of the table to create.
    * @returns Promise which fulfills with the number of changes to the database.
    */
   export async function createTable(txn: TransactionExecutor, tableName: string): Promise<number> {
       const statement: string = `CREATE TABLE ${tableName}`;
       return await txn.execute(statement).then((result: Result) => {
           log(`Successfully created table ${tableName}.`);
           return result.getResultList().length;
       });
   }
   
   /**
    * Create tables in a QLDB ledger.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbDriver: QldbDriver = getQldbDriver();
           await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
               Promise.all([
                   createTable(txn, VEHICLE_REGISTRATION_TABLE_NAME),
                   createTable(txn, VEHICLE_TABLE_NAME),
                   createTable(txn, PERSON_TABLE_NAME),
                   createTable(txn, DRIVERS_LICENSE_TABLE_NAME)
               ]);
           });
       } catch (e) {
           error(`Unable to create tables: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```
**注意**  
该程序演示如何在 QLDB 驱动程序实例中使用 `executeLambda` 函数。在此示例中，您使用单个 lambda 表达式运行多个`CREATE TABLE` PartiQL 语句。  
该执行函数采用隐式启动事务，运行 lambda 中的所有语句，然后自动提交事务。

1. 要运行编译后的程序，请输入以下命令。

   ```
   node dist/CreateTable.js
   ```

1. 如前所述，使用以下程序（`CreateIndex.ts`），以在表上创建索引。

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QldbDriver, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   
   import { getQldbDriver } from "./ConnectToLedger";
   import {
       DRIVERS_LICENSE_TABLE_NAME,
       GOV_ID_INDEX_NAME,
       LICENSE_NUMBER_INDEX_NAME,
       LICENSE_PLATE_NUMBER_INDEX_NAME,
       PERSON_ID_INDEX_NAME,
       PERSON_TABLE_NAME,
       VEHICLE_REGISTRATION_TABLE_NAME,
       VEHICLE_TABLE_NAME,
       VIN_INDEX_NAME
   } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   
   /**
    * Create an index for a particular table.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param tableName Name of the table to add indexes for.
    * @param indexAttribute Index to create on a single attribute.
    * @returns Promise which fulfills with the number of changes to the database.
    */
   export async function createIndex(
       txn: TransactionExecutor, 
       tableName: string, 
       indexAttribute: string
   ): Promise<number> {
       const statement: string = `CREATE INDEX on ${tableName} (${indexAttribute})`;
       return await txn.execute(statement).then((result) => {
           log(`Successfully created index ${indexAttribute} on table ${tableName}.`);
           return result.getResultList().length;
       });
   }
   
   /**
    * Create indexes on tables in a particular ledger.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbDriver: QldbDriver = getQldbDriver();
           await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
               Promise.all([
                   createIndex(txn, PERSON_TABLE_NAME, GOV_ID_INDEX_NAME),
                   createIndex(txn, VEHICLE_TABLE_NAME, VIN_INDEX_NAME),
                   createIndex(txn, VEHICLE_REGISTRATION_TABLE_NAME, VIN_INDEX_NAME),
                   createIndex(txn, VEHICLE_REGISTRATION_TABLE_NAME, LICENSE_PLATE_NUMBER_INDEX_NAME),
                   createIndex(txn, DRIVERS_LICENSE_TABLE_NAME, PERSON_ID_INDEX_NAME),
                   createIndex(txn, DRIVERS_LICENSE_TABLE_NAME, LICENSE_NUMBER_INDEX_NAME)
               ]);
           });
       } catch (e) {
           error(`Unable to create indexes: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```

1. 要运行编译后的程序，请输入以下命令。

   ```
   node dist/CreateIndex.js
   ```

**将样本数据加载到表中**

1. 审查以下 `.ts` 文件。

   1. `SampleData.ts` - 包含您插入到 `vehicle-registration` 表中的示例数据。

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      import { Decimal } from "ion-js";
      
      const EMPTY_SECONDARY_OWNERS: object[] = [];
      export const DRIVERS_LICENSE = [
          {
              PersonId: "",
              LicenseNumber: "LEWISR261LL",
              LicenseType: "Learner",
              ValidFromDate: new Date("2016-12-20"),
              ValidToDate: new Date("2020-11-15")
          },
          {
              PersonId: "",
              LicenseNumber : "LOGANB486CG",
              LicenseType: "Probationary",
              ValidFromDate : new Date("2016-04-06"),
              ValidToDate : new Date("2020-11-15")
          },
          {
              PersonId: "",
              LicenseNumber : "744 849 301",
              LicenseType: "Full",
              ValidFromDate : new Date("2017-12-06"),
              ValidToDate : new Date("2022-10-15")
          },
          {
              PersonId: "",
              LicenseNumber : "P626-168-229-765",
              LicenseType: "Learner",
              ValidFromDate : new Date("2017-08-16"),
              ValidToDate : new Date("2021-11-15")
          },
          {
              PersonId: "",
              LicenseNumber : "S152-780-97-415-0",
              LicenseType: "Probationary",
              ValidFromDate : new Date("2015-08-15"),
              ValidToDate : new Date("2021-08-21")
          }
      ];
      export const PERSON = [
          {
              FirstName : "Raul",
              LastName : "Lewis",
              DOB : new Date("1963-08-19"),
              Address : "1719 University Street, Seattle, WA, 98109",
              GovId : "LEWISR261LL",
              GovIdType : "Driver License"
          },
          {
              FirstName : "Brent",
              LastName : "Logan",
              DOB : new Date("1967-07-03"),
              Address : "43 Stockert Hollow Road, Everett, WA, 98203",
              GovId : "LOGANB486CG",
              GovIdType : "Driver License"
          },
          {
              FirstName : "Alexis",
              LastName : "Pena",
              DOB : new Date("1974-02-10"),
              Address : "4058 Melrose Street, Spokane Valley, WA, 99206",
              GovId : "744 849 301",
              GovIdType : "SSN"
          },
          {
              FirstName : "Melvin",
              LastName : "Parker",
              DOB : new Date("1976-05-22"),
              Address : "4362 Ryder Avenue, Seattle, WA, 98101",
              GovId : "P626-168-229-765",
              GovIdType : "Passport"
          },
          {
              FirstName : "Salvatore",
              LastName : "Spencer",
              DOB : new Date("1997-11-15"),
              Address : "4450 Honeysuckle Lane, Seattle, WA, 98101",
              GovId : "S152-780-97-415-0",
              GovIdType : "Passport"
          }
      ];
      export const VEHICLE = [
          {
              VIN : "1N4AL11D75C109151",
              Type : "Sedan",
              Year : 2011,
              Make : "Audi",
              Model : "A5",
              Color : "Silver"
          },
          {
              VIN : "KM8SRDHF6EU074761",
              Type : "Sedan",
              Year : 2015,
              Make : "Tesla",
              Model : "Model S",
              Color : "Blue"
          },
          {
              VIN : "3HGGK5G53FM761765",
              Type : "Motorcycle",
              Year : 2011,
              Make : "Ducati",
              Model : "Monster 1200",
              Color : "Yellow"
          },
          {
              VIN : "1HVBBAANXWH544237",
              Type : "Semi",
              Year : 2009,
              Make : "Ford",
              Model : "F 150",
              Color : "Black"
          },
          {
              VIN : "1C4RJFAG0FC625797",
              Type : "Sedan",
              Year : 2019,
              Make : "Mercedes",
              Model : "CLK 350",
              Color : "White"
          }
      ];
      export const VEHICLE_REGISTRATION = [
          {
              VIN : "1N4AL11D75C109151",
              LicensePlateNumber : "LEWISR261LL",
              State : "WA",
              City : "Seattle",
              ValidFromDate : new Date("2017-08-21"),
              ValidToDate : new Date("2020-05-11"),
              PendingPenaltyTicketAmount : new Decimal(9025, -2),
              Owners : {
                  PrimaryOwner : { PersonId : "" },
                  SecondaryOwners : EMPTY_SECONDARY_OWNERS
              }
          },
          {
              VIN : "KM8SRDHF6EU074761",
              LicensePlateNumber : "CA762X",
              State : "WA",
              City : "Kent",
              PendingPenaltyTicketAmount : new Decimal(13075, -2),
              ValidFromDate : new Date("2017-09-14"),
              ValidToDate : new Date("2020-06-25"),
              Owners : {
                  PrimaryOwner : { PersonId : "" },
                  SecondaryOwners : EMPTY_SECONDARY_OWNERS
              }
          },
          {
              VIN : "3HGGK5G53FM761765",
              LicensePlateNumber : "CD820Z",
              State : "WA",
              City : "Everett",
              PendingPenaltyTicketAmount : new Decimal(44230, -2),
              ValidFromDate : new Date("2011-03-17"),
              ValidToDate : new Date("2021-03-24"),
              Owners : {
                  PrimaryOwner : { PersonId : "" },
                  SecondaryOwners : EMPTY_SECONDARY_OWNERS
              }
          },
          {
              VIN : "1HVBBAANXWH544237",
              LicensePlateNumber : "LS477D",
              State : "WA",
              City : "Tacoma",
              PendingPenaltyTicketAmount : new Decimal(4220, -2),
              ValidFromDate : new Date("2011-10-26"),
              ValidToDate : new Date("2023-09-25"),
              Owners : {
                  PrimaryOwner : { PersonId : "" },
                  SecondaryOwners : EMPTY_SECONDARY_OWNERS
              }
          },
          {
              VIN : "1C4RJFAG0FC625797",
              LicensePlateNumber : "TH393F",
              State : "WA",
              City : "Olympia",
              PendingPenaltyTicketAmount : new Decimal(3045, -2),
              ValidFromDate : new Date("2013-09-02"),
              ValidToDate : new Date("2024-03-19"),
              Owners : {
                  PrimaryOwner : { PersonId : "" },
                  SecondaryOwners : EMPTY_SECONDARY_OWNERS
              }
          }
      ];
      ```

   1. `Util.ts` - 从 `ion-js` 包导入的实用程序模块，提供用于转换、解析和打印 [Amazon Ion](ion.md) 数据的辅助函数。

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      import { Result, TransactionExecutor } from "amazon-qldb-driver-nodejs";
      import { GetBlockResponse, GetDigestResponse, ValueHolder } from "aws-sdk/clients/qldb";
      import { 
          Decimal, 
          decodeUtf8,
          dom,
          IonTypes, 
          makePrettyWriter, 
          makeReader, 
          Reader, 
          Timestamp, 
          toBase64, 
          Writer
      } from "ion-js";
      
      import { error } from "./LogUtil";
      
      
      
      /**
       * TODO: Replace this with json.stringify
       * Returns the string representation of a given BlockResponse.
       * @param blockResponse The BlockResponse to convert to string.
       * @returns The string representation of the supplied BlockResponse.
       */
      export function blockResponseToString(blockResponse: GetBlockResponse): string {
          let stringBuilder: string = "";
          if (blockResponse.Block.IonText) {
              stringBuilder = stringBuilder + "Block: " + blockResponse.Block.IonText + ", ";
          }
          if (blockResponse.Proof.IonText) {
              stringBuilder = stringBuilder + "Proof: " + blockResponse.Proof.IonText;
          }
          stringBuilder = "{" + stringBuilder + "}";
          const writer: Writer = makePrettyWriter();
          const reader: Reader = makeReader(stringBuilder);
          writer.writeValues(reader);
          return decodeUtf8(writer.getBytes());
      }
      
      /**
       * TODO: Replace this with json.stringify
       * Returns the string representation of a given GetDigestResponse.
       * @param digestResponse The GetDigestResponse to convert to string.
       * @returns The string representation of the supplied GetDigestResponse.
       */
      export function digestResponseToString(digestResponse: GetDigestResponse): string {
          let stringBuilder: string = "";
          if (digestResponse.Digest) {
              stringBuilder += "Digest: " + JSON.stringify(toBase64(<Uint8Array> digestResponse.Digest)) + ", ";
          }
          if (digestResponse.DigestTipAddress.IonText) {
              stringBuilder += "DigestTipAddress: " + digestResponse.DigestTipAddress.IonText;
          }
          stringBuilder = "{" + stringBuilder + "}";
          const writer: Writer = makePrettyWriter();
          const reader: Reader = makeReader(stringBuilder);
          writer.writeValues(reader);
          return decodeUtf8(writer.getBytes());
      }
      
      /**
       * Get the document IDs from the given table.
       * @param txn The {@linkcode TransactionExecutor} for lambda execute.
       * @param tableName The table name to query.
       * @param field A field to query.
       * @param value The key of the given field.
       * @returns Promise which fulfills with the document ID as a string.
       */
      export async function getDocumentId(
          txn: TransactionExecutor,
          tableName: string,
          field: string,
          value: string
      ): Promise<string> {
          const query: string = `SELECT id FROM ${tableName} AS t BY id WHERE t.${field} = ?`;
          let documentId: string = undefined;
          await txn.execute(query, value).then((result: Result) => {
              const resultList: dom.Value[] = result.getResultList();
              if (resultList.length === 0) {
                  throw new Error(`Unable to retrieve document ID using ${value}.`);
              }
              documentId = resultList[0].get("id").stringValue();
      
          }).catch((err: any) => {
              error(`Error getting documentId: ${err}`);
          });
          return documentId;
      }
      
      /**
       * Sleep for the specified amount of time.
       * @param ms The amount of time to sleep in milliseconds.
       * @returns Promise which fulfills with void.
       */
      export function sleep(ms: number): Promise<void> {
          return new Promise(resolve => setTimeout(resolve, ms));
      }
      
      /**
       * Find the value of a given path in an Ion value. The path should contain a blob value.
       * @param value The Ion value that contains the journal block attributes.
       * @param path The path to a certain attribute.
       * @returns Uint8Array value of the blob, or null if the attribute cannot be found in the Ion value
       *                  or is not of type Blob
       */
      export function getBlobValue(value: dom.Value, path: string): Uint8Array | null {
          const attribute: dom.Value = value.get(path);
          if (attribute !== null && attribute.getType() === IonTypes.BLOB) {
              return attribute.uInt8ArrayValue();
          }
          return null;
      }
      
      /**
       * TODO: Replace this with json.stringify
       * Returns the string representation of a given ValueHolder.
       * @param valueHolder The ValueHolder to convert to string.
       * @returns The string representation of the supplied ValueHolder.
       */
      export function valueHolderToString(valueHolder: ValueHolder): string {
          const stringBuilder: string = `{ IonText: ${valueHolder.IonText}}`;
          const writer: Writer = makePrettyWriter();
          const reader: Reader = makeReader(stringBuilder);
          writer.writeValues(reader);
          return decodeUtf8(writer.getBytes());
      }
      
      /**
       * Converts a given value to Ion using the provided writer.
       * @param value The value to convert to Ion.
       * @param ionWriter The Writer to pass the value into.
       * @throws Error: If the given value cannot be converted to Ion.
       */
      export function writeValueAsIon(value: any, ionWriter: Writer): void {
          switch (typeof value) {
              case "string":
                  ionWriter.writeString(value);
                  break;
              case "boolean":
                  ionWriter.writeBoolean(value);
                  break;
              case "number":
                      ionWriter.writeInt(value);
                      break;
              case "object":
                  if (Array.isArray(value)) {
                      // Object is an array.
                      ionWriter.stepIn(IonTypes.LIST);
      
                      for (const element of value) {
                          writeValueAsIon(element, ionWriter);
                      }
      
                      ionWriter.stepOut();
                  } else if (value instanceof Date) {
                      // Object is a Date.
                      ionWriter.writeTimestamp(Timestamp.parse(value.toISOString()));
                  } else if (value instanceof Decimal) {
                      // Object is a Decimal.
                      ionWriter.writeDecimal(value);
                  } else if (value === null) {
                      ionWriter.writeNull(IonTypes.NULL);
                  } else {
                      // Object is a struct.
                      ionWriter.stepIn(IonTypes.STRUCT);
      
                      for (const key of Object.keys(value)) {
                          ionWriter.writeFieldName(key);
                          writeValueAsIon(value[key], ionWriter);
                      }
                      ionWriter.stepOut();
                  }
                  break;
              default:
                  throw new Error(`Cannot convert to Ion for type: ${(typeof value)}.`);
          }
      }
      ```
**注意**  
该`getDocumentId`函数运行一个查询， IDs 从表中返回系统分配的文档。要了解更多信息，请参阅 [通过 BY 子句查询文档 ID](working.metadata.by-clause.md)。

1. 使用以下程序（`InsertDocument.ts`），将示例数据插入表内。

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   import { dom } from "ion-js";
   
   import { getQldbDriver } from "./ConnectToLedger";
   import { DRIVERS_LICENSE, PERSON, VEHICLE, VEHICLE_REGISTRATION } from "./model/SampleData";
   import {
       DRIVERS_LICENSE_TABLE_NAME,
       PERSON_TABLE_NAME,
       VEHICLE_REGISTRATION_TABLE_NAME,
       VEHICLE_TABLE_NAME
   } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   
   /**
    * Insert the given list of documents into a table in a single transaction.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param tableName Name of the table to insert documents into.
    * @param documents List of documents to insert.
    * @returns Promise which fulfills with a {@linkcode Result} object.
    */
   export async function insertDocument(
       txn: TransactionExecutor,
       tableName: string,
       documents: object[]
   ): Promise<Result> {
       const statement: string = `INSERT INTO ${tableName} ?`;
       const result: Result = await txn.execute(statement, documents);
       return result;
   }
   
   /**
    * Handle the insertion of documents and updating PersonIds all in a single transaction.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @returns Promise which fulfills with void.
    */
   async function updateAndInsertDocuments(txn: TransactionExecutor): Promise<void> {
       log("Inserting multiple documents into the 'Person' table...");
       const documentIds: Result = await insertDocument(txn, PERSON_TABLE_NAME, PERSON);
   
       const listOfDocumentIds: dom.Value[] = documentIds.getResultList();
       log("Updating PersonIds for 'DriversLicense' and PrimaryOwner for 'VehicleRegistration'...");
       updatePersonId(listOfDocumentIds);
   
       log("Inserting multiple documents into the remaining tables...");
       await Promise.all([
           insertDocument(txn, DRIVERS_LICENSE_TABLE_NAME, DRIVERS_LICENSE),
           insertDocument(txn, VEHICLE_REGISTRATION_TABLE_NAME, VEHICLE_REGISTRATION),
           insertDocument(txn, VEHICLE_TABLE_NAME, VEHICLE)
       ]);
   }
   
   /**
    * Update the PersonId value for DriversLicense records and the PrimaryOwner value for VehicleRegistration records.
    * @param documentIds List of document IDs.
    */
   export function updatePersonId(documentIds: dom.Value[]): void {
       documentIds.forEach((value: dom.Value, i: number) => {
           const documentId: string = value.get("documentId").stringValue();
           DRIVERS_LICENSE[i].PersonId = documentId;
           VEHICLE_REGISTRATION[i].Owners.PrimaryOwner.PersonId = documentId;
       });
   }
   
   /**
    * Insert documents into a table in a QLDB ledger.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbDriver: QldbDriver = getQldbDriver();
           await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
               await updateAndInsertDocuments(txn);
           });
       } catch (e) {
           error(`Unable to insert documents: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```
**注意**  
该程序演示如何使用参数化值调用`execute`方法。除了要运行的 PartiQL 语句之外，您还可以传递数据参数。在语句字符串中将问号（`?`）作为变量占位符。
如果 `INSERT` 语句成功，则返回每个插入文档的`id`。

1. 要运行编译后的程序，请输入以下命令。

   ```
   node dist/InsertDocument.js
   ```

接下来，您可以使用 `SELECT` 语句从 `vehicle-registration` 分类账中的表中读取数据。继续执行[步骤 4：查询分类账中的表](getting-started.nodejs.step-4.md)。

# 步骤 4：查询分类账中的表
<a name="getting-started.nodejs.step-4"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在 Amazon QLDB 分类账中创建表格和加载数据后，您可运行查询以查看刚刚插入的车辆登记数据。QLDB 使用 [PartiQL](ql-reference.md)作为其查询语言，使用 [Amazon Ion](ion.md)作为其面向文档的数据模型。

PartiQL 是开源、与 SQL 兼容的查询语言，现已扩展为可与 Ion 配合使用。使用 PartiQL，您可使用熟悉的 SQL 运算符插入、查询和管理数据。Amazon Ion 是 JSON 的超集。Ion 是基于文档的开源数据格式，可让您灵活地存储和处理结构化、半结构化和嵌套数据。

在此步骤中，您将使用 `SELECT` 语句从 `vehicle-registration`分类账中的表中读取数据。

**警告**  
当您在没有索引查找的情况下运行查询时，它会调用全表扫描。PartiQL 之所以支持此类查询，是因为其与 SQL 兼容。但是，*切勿*在 QLDB 中对生产用例运行表扫描。表扫描可能会导致大型表出现性能问题，包括并发冲突与事务超时。  
为避免表扫描，必须在索引字段或文档 ID 上使用*相等*运算符（`WHERE indexedField = 123`或`WHERE indexedField IN (456, 789)`）运行带有`WHERE`谓词子句的语句。有关更多信息，请参阅 [优化查询性能](working.optimize.md)。

**查询表格**

1. 使用以下程序（`FindVehicles.ts`），以查询分类账中某人名下注册的所有车辆。

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   import { dom } from "ion-js";
   
   import { getQldbDriver } from "./ConnectToLedger";
   import { PERSON } from "./model/SampleData";
   import { PERSON_TABLE_NAME } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   import { getDocumentId } from "./qldb/Util";
   import { prettyPrintResultList } from "./ScanTable";
   
   /**
    * Query 'Vehicle' and 'VehicleRegistration' tables using a unique document ID in one transaction.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param govId The owner's government ID.
    * @returns Promise which fulfills with void.
    */
   async function findVehiclesForOwner(txn: TransactionExecutor, govId: string): Promise<void> {
       const documentId: string = await getDocumentId(txn, PERSON_TABLE_NAME, "GovId", govId);
       const query: string = "SELECT Vehicle FROM Vehicle INNER JOIN VehicleRegistration AS r " +
                           "ON Vehicle.VIN = r.VIN WHERE r.Owners.PrimaryOwner.PersonId = ?";
   
       await txn.execute(query, documentId).then((result: Result) => {
           const resultList: dom.Value[] = result.getResultList();
           log(`List of vehicles for owner with GovId: ${govId}`);
           prettyPrintResultList(resultList);
       });
   }
   
   /**
    * Find all vehicles registered under a person.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbDriver: QldbDriver = getQldbDriver();
           await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
               await findVehiclesForOwner(txn, PERSON[0].GovId);
           });
       } catch (e) {
           error(`Error getting vehicles for owner: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```
**注意**  
首先，该程序在 `Person` 表格中查询文档`GovId LEWISR261LL`，以获取其`id`元数据字段。  
然后，它使用文档`id`，通过`PrimaryOwner.PersonId`作为外键查询`VehicleRegistration`表。它还结合`VIN`字段上的`Vehicle`表的`VehicleRegistration`。

1. 要运行编译后的程序，请输入以下命令。

   ```
   node dist/FindVehicles.js
   ```

要了解如何修改 `vehicle-registration` 分类账表格中的文档，请参阅 [第 5 步：修改分类账中的文档](getting-started.nodejs.step-5.md)。

# 第 5 步：修改分类账中的文档
<a name="getting-started.nodejs.step-5"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

现在您有了要处理的数据，可以开始在 Amazon QLDB 中对 `vehicle-registration` 分类账文档进行更改。在此步骤中，以下代码示例介绍了如何运行数据操作语言（DML）语句。这些声明更新一辆车的主要车主，并为另一辆车添加了次要车主。

**修改文档**

1. 使用以下程序（`TransferVehicleOwnership.ts`），更新分类账中带有 VIN `1N4AL11D75C109151` 的车辆的主要车主。

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   import { dom } from "ion-js";
   
   import { getQldbDriver } from "./ConnectToLedger";
   import { PERSON, VEHICLE } from "./model/SampleData";
   import { PERSON_TABLE_NAME } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   import { getDocumentId } from "./qldb/Util";
   
   /**
    * Query a driver's information using the given ID.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param documentId The unique ID of a document in the Person table.
    * @returns Promise which fulfills with an Ion value containing the person.
    */
   export async function findPersonFromDocumentId(txn: TransactionExecutor, documentId: string): Promise<dom.Value> {
       const query: string = "SELECT p.* FROM Person AS p BY pid WHERE pid = ?";
   
       let personId: dom.Value;
       await txn.execute(query, documentId).then((result: Result) => {
           const resultList: dom.Value[] = result.getResultList();
           if (resultList.length === 0) {
               throw new Error(`Unable to find person with ID: ${documentId}.`);
           }
           personId = resultList[0];
       });
       return personId;
   }
   
   /**
    * Find the primary owner for the given VIN.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param vin The VIN to find primary owner for.
    * @returns Promise which fulfills with an Ion value containing the primary owner.
    */
   export async function findPrimaryOwnerForVehicle(txn: TransactionExecutor, vin: string): Promise<dom.Value> {
       log(`Finding primary owner for vehicle with VIN: ${vin}`);
       const query: string = "SELECT Owners.PrimaryOwner.PersonId FROM VehicleRegistration AS v WHERE v.VIN = ?";
   
       let documentId: string = undefined;
       await txn.execute(query, vin).then((result: Result) => {
           const resultList: dom.Value[] = result.getResultList();
           if (resultList.length === 0) {
               throw new Error(`Unable to retrieve document ID using ${vin}.`);
           }
           const PersonIdValue: dom.Value = resultList[0].get("PersonId");
           if (PersonIdValue === null) {
               throw new Error(`Expected field name PersonId not found.`);
           }
           documentId = PersonIdValue.stringValue();
       });
       return findPersonFromDocumentId(txn, documentId);
   }
   
   /**
    * Update the primary owner for a vehicle using the given VIN.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param vin The VIN for the vehicle to operate on.
    * @param documentId New PersonId for the primary owner.
    * @returns Promise which fulfills with void.
    */
   async function updateVehicleRegistration(txn: TransactionExecutor, vin: string, documentId: string): Promise<void> {
       const statement: string = "UPDATE VehicleRegistration AS r SET r.Owners.PrimaryOwner.PersonId = ? WHERE r.VIN = ?";
   
       log(`Updating the primary owner for vehicle with VIN: ${vin}...`);
       await txn.execute(statement, documentId, vin).then((result: Result) => {
           const resultList: dom.Value[] = result.getResultList();
           if (resultList.length === 0) {
               throw new Error("Unable to transfer vehicle, could not find registration.");
           }
           log(`Successfully transferred vehicle with VIN ${vin} to new owner.`);
       });
   }
   
   /**
    * Validate the current owner of the given vehicle and transfer its ownership to a new owner in a single transaction.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param vin The VIN of the vehicle to transfer ownership of.
    * @param currentOwner The GovId of the current owner of the vehicle.
    * @param newOwner The GovId of the new owner of the vehicle.
    */
   export async function validateAndUpdateRegistration(
       txn: TransactionExecutor,
       vin: string,
       currentOwner: string,
       newOwner: string
   ): Promise<void> {
       const primaryOwner: dom.Value = await findPrimaryOwnerForVehicle(txn, vin);
       const govIdValue: dom.Value = primaryOwner.get("GovId");
       if (govIdValue !== null && govIdValue.stringValue() !== currentOwner) {
           log("Incorrect primary owner identified for vehicle, unable to transfer.");
       }
       else {
           const documentId: string = await getDocumentId(txn, PERSON_TABLE_NAME, "GovId", newOwner);
           await updateVehicleRegistration(txn, vin, documentId);
           log("Successfully transferred vehicle ownership!");
       }
   }
   
   /**
    * Find primary owner for a particular vehicle's VIN.
    * Transfer to another primary owner for a particular vehicle's VIN.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbDriver: QldbDriver = getQldbDriver();
   
           const vin: string = VEHICLE[0].VIN;
           const previousOwnerGovId: string = PERSON[0].GovId;
           const newPrimaryOwnerGovId: string = PERSON[1].GovId;
   
           await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
               await validateAndUpdateRegistration(txn, vin, previousOwnerGovId,  newPrimaryOwnerGovId);
           });
       } catch (e) {
           error(`Unable to connect and run queries: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```

1. 要运行编译后的程序，请输入以下命令。

   ```
   node dist/TransferVehicleOwnership.js
   ```

1. 使用以下程序（`AddSecondaryOwner.ts`），将二级车主添加至分类账中带有 VIN `KM8SRDHF6EU074761` 的车辆中。

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   import { dom } from "ion-js";
   
   import { getQldbDriver } from "./ConnectToLedger";
   import { PERSON, VEHICLE_REGISTRATION } from "./model/SampleData";
   import { PERSON_TABLE_NAME } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   import { getDocumentId } from "./qldb/Util";
   import { prettyPrintResultList } from "./ScanTable";
   
   /**
    * Add a secondary owner into 'VehicleRegistration' table for a particular VIN.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param vin VIN of the vehicle to query.
    * @param secondaryOwnerId The secondary owner's person ID.
    * @returns Promise which fulfills with void.
    */
   export async function addSecondaryOwner(
       txn: TransactionExecutor, 
       vin: string, 
       secondaryOwnerId: string
   ): Promise<void> {
       log(`Inserting secondary owner for vehicle with VIN: ${vin}`);
       const query: string =
           `FROM VehicleRegistration AS v WHERE v.VIN = ? INSERT INTO v.Owners.SecondaryOwners VALUE ?`;
   
       const personToInsert = {PersonId: secondaryOwnerId};
       await txn.execute(query, vin, personToInsert).then(async (result: Result) => {
           const resultList: dom.Value[] = result.getResultList();
           log("VehicleRegistration Document IDs which had secondary owners added: ");
           prettyPrintResultList(resultList);
       });
   }
   
   /**
    * Query for a document ID with a government ID.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param governmentId The government ID to query with.
    * @returns Promise which fulfills with the document ID as a string.
    */
   export async function getDocumentIdByGovId(txn: TransactionExecutor, governmentId: string): Promise<string> {
       const documentId: string = await getDocumentId(txn, PERSON_TABLE_NAME, "GovId", governmentId);
       return documentId;
   }
   
   /**
    * Check whether a driver has already been registered for the given VIN.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param vin VIN of the vehicle to query.
    * @param secondaryOwnerId The secondary owner's person ID.
    * @returns Promise which fulfills with a boolean.
    */
   export async function isSecondaryOwnerForVehicle(
       txn: TransactionExecutor,
       vin: string,
       secondaryOwnerId: string
   ): Promise<boolean> {
       log(`Finding secondary owners for vehicle with VIN: ${vin}`);
       const query: string = "SELECT Owners.SecondaryOwners FROM VehicleRegistration AS v WHERE v.VIN = ?";
   
       let doesExist: boolean = false;
   
       await txn.execute(query, vin).then((result: Result) => {
           const resultList: dom.Value[] = result.getResultList();
   
           resultList.forEach((value: dom.Value) => {
               const secondaryOwnersList: dom.Value[] = value.get("SecondaryOwners").elements();
   
               secondaryOwnersList.forEach((secondaryOwner) => {
                   const personId: dom.Value = secondaryOwner.get("PersonId");
                   if (personId !== null &&  personId.stringValue() === secondaryOwnerId) {
                       doesExist = true;
                   }
               });
           });
       });
       return doesExist;
   }
   
   /**
    * Finds and adds secondary owners for a vehicle.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbDriver: QldbDriver = getQldbDriver();
           const vin: string = VEHICLE_REGISTRATION[1].VIN;
           const govId: string = PERSON[0].GovId;
   
           await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
               const documentId: string = await getDocumentIdByGovId(txn, govId);
   
               if (await isSecondaryOwnerForVehicle(txn, vin, documentId)) {
                   log(`Person with ID ${documentId} has already been added as a secondary owner of this vehicle.`);
               } else {
                   await addSecondaryOwner(txn, vin, documentId);
               }
           });
   
           log("Secondary owners successfully updated.");
       } catch (e) {
           error(`Unable to add secondary owner: ${e}`);
       } 
   }
   
   if (require.main === module) {
       main();
   }
   ```

1. 要运行编译后的程序，请输入以下命令。

   ```
   node dist/AddSecondaryOwner.js
   ```

若要查看 `vehicle-registration` 分类账中的这些更改，请参阅[步骤 6：查看文档修订历史记录](getting-started.nodejs.step-6.md)。

# 步骤 6：查看文档修订历史记录
<a name="getting-started.nodejs.step-6"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在[上一步](getting-started.nodejs.step-5.md)中修改车辆注册数据后，您可查询其所有注册车主的历史记录以及任何其他更新的字段。在此步骤中，您将在 `vehicle-registration` 分类账的 `VehicleRegistration` 表格中查询文档的修订历史记录。

**若要查看修订历史记录**

1. 编译并运行 `QueryHistory.ts` 程序，以使用 VIN `1N4AL11D75C109151`查询`VehicleRegistration`文档的修订历史记录。

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   import { dom } from "ion-js";
   
   import { getQldbDriver } from "./ConnectToLedger";
   import { VEHICLE_REGISTRATION } from "./model/SampleData";
   import { VEHICLE_REGISTRATION_TABLE_NAME } from "./qldb/Constants";
   import { prettyPrintResultList } from "./ScanTable";
   import { error, log } from "./qldb/LogUtil";
   import { getDocumentId } from "./qldb/Util";
   
   /**
    * Find previous primary owners for the given VIN in a single transaction.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param vin The VIN to find previous primary owners for.
    * @returns Promise which fulfills with void.
    */
   async function previousPrimaryOwners(txn: TransactionExecutor, vin: string): Promise<void> {
       const documentId: string = await getDocumentId(txn, VEHICLE_REGISTRATION_TABLE_NAME, "VIN", vin);
       const todaysDate: Date = new Date();
       // set todaysDate back one minute to ensure end time is in the past
       // by the time the request reaches our backend
       todaysDate.setMinutes(todaysDate.getMinutes() - 1);
       const threeMonthsAgo: Date = new Date(todaysDate);
       threeMonthsAgo.setMonth(todaysDate.getMonth() - 3);
   
       const query: string =
           `SELECT data.Owners.PrimaryOwner, metadata.version FROM history ` +
           `(${VEHICLE_REGISTRATION_TABLE_NAME}, \`${threeMonthsAgo.toISOString()}\`, \`${todaysDate.toISOString()}\`) ` +
           `AS h WHERE h.metadata.id = ?`;
   
       await txn.execute(query, documentId).then((result: Result) => {
           log(`Querying the 'VehicleRegistration' table's history using VIN: ${vin}.`);
           const resultList: dom.Value[] = result.getResultList();
           prettyPrintResultList(resultList);
       });
   }
   
   /**
    * Query a table's history for a particular set of documents.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbDriver: QldbDriver = getQldbDriver();
           const vin: string = VEHICLE_REGISTRATION[0].VIN;
           await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
               await previousPrimaryOwners(txn, vin);
           });
       } catch (e) {
           error(`Unable to query history to find previous owners: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```
**注意**  
您可以通过以下语法查询内置版本[历史记录函数](working.history.md#working.history.function)，以查看文档的修订历史记录。  

     ```
     SELECT * FROM history( table_name [, `start-time` [, `end-time` ] ] ) AS h
     [ WHERE h.metadata.id = 'id' ]
     ```
*开始时间*和*结束时间*均为可选。它们是 Amazon Ion 的字面值，可以用反引号（``...``）表示。有关更多信息，请参阅 [在 Amazon QLDB 中使用 PartiQL 查询 Ion](ql-reference.query.md)。
最佳做法是，使用日期范围（*开始时间* 和 *结束时间*）和文档 ID（`metadata.id`）来限定历史记录查询。QLDB 处理事务中的`SELECT`查询，这些查询受[事务超时限制](limits.md#limits.fixed)的约束。  
QLDB 历史记录按文档 ID 编制索引，目前无法创建其他历史索引。包含开始时间和结束时间的历史记录查询将从日期范围限定中获得便利。

1. 要运行编译后的程序，请输入以下命令。

   ```
   node dist/QueryHistory.js
   ```

要以加密方式验证 `vehicle-registration` 分类账中的文档修订版，请继续 [第 7 步：验证分类账中的文档](getting-started.nodejs.step-7.md)。

# 第 7 步：验证分类账中的文档
<a name="getting-started.nodejs.step-7"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

借助 Amazon QLDB，您可在 SHA-256 中使用加密哈希，从而有效地验证分类账日记账中文档的完整性。要详细了解验证和加密哈希在 QLDB 中的工作原理，请参阅 [Amazon QLDB 中的数据验证](verification.md)。

在此步骤中，您将验证 `vehicle-registration` 分类账 `VehicleRegistration` 表格中的单据修订版本。首先，您请求一份摘要，该摘要作为输出文件返回，并作为分类账整个变更历史记录的签名。然后，您要求提供与此摘要相关的修订证明。使用此证明，如果所有验证检查都可通过，可以验证修订版的完整性。

**验证文档的修订版**

1. 查看以下 `.ts` 文件，其中包含验证所需的 QLDB 对象。

   1. `BlockAddress.ts`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      import { ValueHolder } from "aws-sdk/clients/qldb";
      import { dom, IonTypes } from "ion-js";
      
      export class BlockAddress {
          _strandId: string;
          _sequenceNo: number;
      
          constructor(strandId: string, sequenceNo: number) {
              this._strandId = strandId;
              this._sequenceNo = sequenceNo;
          }
      }
      
      /**
       * Convert a block address from an Ion value into a ValueHolder.
       * Shape of the ValueHolder must be: {'IonText': "{strandId: <"strandId">, sequenceNo: <sequenceNo>}"}
       * @param value The Ion value that contains the block address values to convert.
       * @returns The ValueHolder that contains the strandId and sequenceNo.
       */
      export function blockAddressToValueHolder(value: dom.Value): ValueHolder {
          const blockAddressValue : dom.Value = getBlockAddressValue(value);
          const strandId: string = getStrandId(blockAddressValue);
          const sequenceNo: number = getSequenceNo(blockAddressValue);
          const valueHolder: string = `{strandId: "${strandId}", sequenceNo: ${sequenceNo}}`;
          const blockAddress: ValueHolder = {IonText: valueHolder};
          return blockAddress;
      }
      
      /**
       * Helper method that to get the Metadata ID.
       * @param value The Ion value.
       * @returns The Metadata ID.
       */
      export function getMetadataId(value: dom.Value): string {
          const metaDataId: dom.Value = value.get("id");
          if (metaDataId === null) {
              throw new Error(`Expected field name id, but not found.`);
          }
          return metaDataId.stringValue();
      }
      
      /**
       * Helper method to get the Sequence No.
       * @param value The Ion value.
       * @returns The Sequence No.
       */
      export function getSequenceNo(value : dom.Value): number {
          const sequenceNo: dom.Value = value.get("sequenceNo");
          if (sequenceNo === null) {
              throw new Error(`Expected field name sequenceNo, but not found.`);
          }
          return sequenceNo.numberValue();
      }
      
      /**
       * Helper method to get the Strand ID.
       * @param value The Ion value.
       * @returns The Strand ID.
       */
      export function getStrandId(value: dom.Value): string {
          const strandId: dom.Value = value.get("strandId");
          if (strandId === null) {
              throw new Error(`Expected field name strandId, but not found.`);
          }
          return strandId.stringValue();
      }
      
      export function getBlockAddressValue(value: dom.Value) : dom.Value {
          const type = value.getType();
          if (type !== IonTypes.STRUCT) {
              throw new Error(`Unexpected format: expected struct, but got IonType: ${type.name}`);
          }
          const blockAddress: dom.Value = value.get("blockAddress");
          if (blockAddress == null) {
              throw new Error(`Expected field name blockAddress, but not found.`);
          }
          return blockAddress;
      }
      ```

   1. `Verifier.ts`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      import { Digest, ValueHolder } from "aws-sdk/clients/qldb";
      import { createHash } from "crypto";
      import { dom, toBase64 } from "ion-js";
      
      import { getBlobValue } from "./Util";
      
      const HASH_LENGTH: number = 32;
      const UPPER_BOUND: number = 8;
      
      /**
       * Build the candidate digest representing the entire ledger from the Proof hashes.
       * @param proof The Proof object.
       * @param leafHash The revision hash to pair with the first hash in the Proof hashes list.
       * @returns The calculated root hash.
       */
      function buildCandidateDigest(proof: ValueHolder, leafHash: Uint8Array): Uint8Array {
          const parsedProof: Uint8Array[] = parseProof(proof);
          const rootHash: Uint8Array = calculateRootHashFromInternalHash(parsedProof, leafHash);
          return rootHash;
      }
      
      /**
       * Combine the internal hashes and the leaf hash until only one root hash remains.
       * @param internalHashes An array of hash values.
       * @param leafHash The revision hash to pair with the first hash in the Proof hashes list.
       * @returns The root hash constructed by combining internal hashes.
       */
      function calculateRootHashFromInternalHash(internalHashes: Uint8Array[], leafHash: Uint8Array): Uint8Array {
          const rootHash: Uint8Array = internalHashes.reduce(joinHashesPairwise, leafHash);
          return rootHash;
      }
      
      /**
       * Compare two hash values by converting each Uint8Array byte, which is unsigned by default,
       * into a signed byte, assuming they are little endian.
       * @param hash1 The hash value to compare.
       * @param hash2 The hash value to compare.
       * @returns Zero if the hash values are equal, otherwise return the difference of the first pair of non-matching bytes.
       */
      function compareHashValues(hash1: Uint8Array, hash2: Uint8Array): number {
          if (hash1.length !== HASH_LENGTH || hash2.length !== HASH_LENGTH) {
              throw new Error("Invalid hash.");
          }
          for (let i = hash1.length-1; i >= 0; i--) {
              const difference: number = (hash1[i]<<24 >>24) - (hash2[i]<<24 >>24);
              if (difference !== 0) {
                  return difference;
              }
          }
          return 0;
      }
      
      /**
       * Helper method that concatenates two Uint8Array.
       * @param arrays List of array to concatenate, in the order provided.
       * @returns The concatenated array.
       */
      function concatenate(...arrays: Uint8Array[]): Uint8Array {
          let totalLength = 0;
          for (const arr of arrays) {
              totalLength += arr.length;
          }
          const result = new Uint8Array(totalLength);
          let offset = 0;
          for (const arr of arrays) {
              result.set(arr, offset);
              offset += arr.length;
          }
          return result;
      }
      
      /**
       * Flip a single random bit in the given hash value.
       * This method is intended to be used for purpose of demonstrating the QLDB verification features only.
       * @param original The hash value to alter.
       * @returns The altered hash with a single random bit changed.
       */
      export function flipRandomBit(original: any): Uint8Array {
          if (original.length === 0) {
              throw new Error("Array cannot be empty!");
          }
          const bytePos: number = Math.floor(Math.random() * original.length);
          const bitShift: number = Math.floor(Math.random() * UPPER_BOUND);
          const alteredHash: Uint8Array = original;
      
          alteredHash[bytePos] = alteredHash[bytePos] ^ (1 << bitShift);
          return alteredHash;
      }
      
      /**
       * Take two hash values, sort them, concatenate them, and generate a new hash value from the concatenated values.
       * @param h1 Byte array containing one of the hashes to compare.
       * @param h2 Byte array containing one of the hashes to compare.
       * @returns The concatenated array of hashes.
       */
      export function joinHashesPairwise(h1: Uint8Array, h2: Uint8Array): Uint8Array {
          if (h1.length === 0) {
              return h2;
          }
          if (h2.length === 0) {
              return h1;
          }
          let concat: Uint8Array;
          if (compareHashValues(h1, h2) < 0) {
              concat = concatenate(h1, h2);
          } else {
              concat = concatenate(h2, h1);
          }
          const hash = createHash('sha256');
          hash.update(concat);
          const newDigest: Uint8Array = hash.digest();
          return newDigest;
      }
      
      /**
       * Parse the Block object returned by QLDB and retrieve block hash.
       * @param valueHolder A structure containing an Ion string value.
       * @returns The block hash.
       */
      export function parseBlock(valueHolder: ValueHolder): Uint8Array {
          const block: dom.Value = dom.load(valueHolder.IonText);
          const blockHash: Uint8Array = getBlobValue(block, "blockHash");
          return blockHash;
      }
      
      /**
       * Parse the Proof object returned by QLDB into an iterator.
       * The Proof object returned by QLDB is a dictionary like the following:
       * {'IonText': '[{{<hash>}},{{<hash>}}]'}
       * @param valueHolder A structure containing an Ion string value.
       * @returns A list of hash values.
       */
      function parseProof(valueHolder: ValueHolder): Uint8Array[] {
          const proofs : dom.Value = dom.load(valueHolder.IonText);
          return proofs.elements().map(proof => proof.uInt8ArrayValue());
      }
      
      /**
       *  Verify document revision against the provided digest.
       * @param documentHash The SHA-256 value representing the document revision to be verified.
       * @param digest The SHA-256 hash value representing the ledger digest.
       * @param proof The Proof object retrieved from GetRevision.getRevision.
       * @returns If the document revision verifies against the ledger digest.
       */
      export function verifyDocument(documentHash: Uint8Array, digest: Digest, proof: ValueHolder): boolean {
          const candidateDigest = buildCandidateDigest(proof, documentHash);
          return (toBase64(<Uint8Array> digest) === toBase64(candidateDigest));
      }
      ```

1. 使用两个`.ts`程序（`GetDigest.ts`和`GetRevision.ts`）执行以下步骤：
   + 从 `vehicle-registration` 分类账中请求新摘要。
   + 通过 `VehicleRegistration` 表的 VIN `1N4AL11D75C109151`，索取每份文件修订版的证明。
   + 通过重新计算摘要，使用返回的摘要来验证修订版。

   `GetDigest.ts` 文件包含以下代码。

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QLDB } from "aws-sdk";
   import { GetDigestRequest, GetDigestResponse } from "aws-sdk/clients/qldb";
   
   import { LEDGER_NAME } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   import { digestResponseToString } from "./qldb/Util";
   
   /**
    * Get the digest of a ledger's journal.
    * @param ledgerName Name of the ledger to operate on.
    * @param qldbClient The QLDB control plane client to use.
    * @returns Promise which fulfills with a GetDigestResponse.
    */
   export async function getDigestResult(ledgerName: string, qldbClient: QLDB): Promise<GetDigestResponse> {
       const request: GetDigestRequest = {
           Name: ledgerName
       };
       const result: GetDigestResponse = await qldbClient.getDigest(request).promise();
       return result;
   }
   
   /**
    * This is an example for retrieving the digest of a particular ledger.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbClient: QLDB = new QLDB();
           log(`Retrieving the current digest for ledger: ${LEDGER_NAME}.`);
           const digest: GetDigestResponse = await getDigestResult(LEDGER_NAME, qldbClient);
           log(`Success. Ledger digest: \n${digestResponseToString(digest)}.`);
       } catch (e) {
           error(`Unable to get a ledger digest: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```
**注意**  
使用该 `getDigest` 方法请求一份涵盖分类账中日记账当前*提示*的摘要。日记账的提示指的是 QLDB 收到您的请求时的最近提交的数据块。

   `GetRevision.ts` 文件包含以下代码。

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QldbDriver, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   import { QLDB } from "aws-sdk";
   import { Digest, GetDigestResponse, GetRevisionRequest, GetRevisionResponse, ValueHolder } from "aws-sdk/clients/qldb";
   import { dom, toBase64 } from "ion-js";
   
   import { getQldbDriver } from "./ConnectToLedger";
   import { getDigestResult } from './GetDigest';
   import { VEHICLE_REGISTRATION } from "./model/SampleData"
   import { blockAddressToValueHolder, getMetadataId } from './qldb/BlockAddress';
   import { LEDGER_NAME } from './qldb/Constants';
   import { error, log } from "./qldb/LogUtil";
   import { getBlobValue, valueHolderToString } from "./qldb/Util";
   import { flipRandomBit, verifyDocument } from "./qldb/Verifier";
   
   /**
    * Get the revision data object for a specified document ID and block address.
    * Also returns a proof of the specified revision for verification.
    * @param ledgerName Name of the ledger containing the document to query.
    * @param documentId Unique ID for the document to be verified, contained in the committed view of the document.
    * @param blockAddress The location of the block to request.
    * @param digestTipAddress The latest block location covered by the digest.
    * @param qldbClient The QLDB control plane client to use.
    * @returns Promise which fulfills with a GetRevisionResponse.
    */
   async function getRevision(
       ledgerName: string,
       documentId: string,
       blockAddress: ValueHolder,
       digestTipAddress: ValueHolder,
       qldbClient: QLDB
   ): Promise<GetRevisionResponse> {
       const request: GetRevisionRequest = {
           Name: ledgerName,
           BlockAddress: blockAddress,
           DocumentId: documentId,
           DigestTipAddress: digestTipAddress
       };
       const result: GetRevisionResponse = await qldbClient.getRevision(request).promise();
       return result;
   }
   
   /**
    * Query the table metadata for a particular vehicle for verification.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param vin VIN to query the table metadata of a specific registration with.
    * @returns Promise which fulfills with a list of Ion values that contains the results of the query.
    */
   export async function lookupRegistrationForVin(txn: TransactionExecutor, vin: string): Promise<dom.Value[]> {
       log(`Querying the 'VehicleRegistration' table for VIN: ${vin}...`);
       let resultList: dom.Value[];
       const query: string = "SELECT blockAddress, metadata.id FROM _ql_committed_VehicleRegistration WHERE data.VIN = ?";
   
       await txn.execute(query, vin).then(function(result) {
         resultList = result.getResultList();
       });
       return resultList;
   }
   
   /**
    * Verify each version of the registration for the given VIN.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param ledgerName The ledger to get the digest from.
    * @param vin VIN to query the revision history of a specific registration with.
    * @param qldbClient The QLDB control plane client to use.
    * @returns Promise which fulfills with void.
    * @throws Error: When verification fails.
    */
   export async function verifyRegistration(
       txn: TransactionExecutor, 
       ledgerName: string, 
       vin: string,
       qldbClient: QLDB
   ): Promise<void> {
       log(`Let's verify the registration with VIN = ${vin}, in ledger = ${ledgerName}.`);
       const digest: GetDigestResponse = await getDigestResult(ledgerName, qldbClient);
       const digestBytes: Digest = digest.Digest;
       const digestTipAddress: ValueHolder = digest.DigestTipAddress;
   
       log(
           `Got a ledger digest: digest tip address = \n${valueHolderToString(digestTipAddress)},
           digest = \n${toBase64(<Uint8Array> digestBytes)}.`
       );
       log(`Querying the registration with VIN = ${vin} to verify each version of the registration...`);
       const resultList: dom.Value[] = await lookupRegistrationForVin(txn, vin);
       log("Getting a proof for the document.");
   
       for (const result of resultList) {
           const blockAddress: ValueHolder =  blockAddressToValueHolder(result);
           const documentId: string = getMetadataId(result);
   
           const revisionResponse: GetRevisionResponse = await getRevision(
               ledgerName, 
               documentId, 
               blockAddress, 
               digestTipAddress,
               qldbClient
           );
   
           const revision: dom.Value = dom.load(revisionResponse.Revision.IonText);
           const documentHash: Uint8Array = getBlobValue(revision, "hash");
           const proof: ValueHolder = revisionResponse.Proof;
           log(`Got back a proof: ${valueHolderToString(proof)}.`);
   
           let verified: boolean = verifyDocument(documentHash, digestBytes, proof);
           if (!verified) {
              throw new Error("Document revision is not verified.");
           } else {
               log("Success! The document is verified.");
           }
           const alteredDocumentHash: Uint8Array = flipRandomBit(documentHash);
   
           log(
               `Flipping one bit in the document's hash and assert that the document is NOT verified.
               The altered document hash is: ${toBase64(alteredDocumentHash)}`
           );
           verified = verifyDocument(alteredDocumentHash, digestBytes, proof);
   
           if (verified) {
               throw new Error("Expected altered document hash to not be verified against digest.");
           } else {
               log("Success! As expected flipping a bit in the document hash causes verification to fail.");
           }
           log(`Finished verifying the registration with VIN = ${vin} in ledger = ${ledgerName}.`);
       }
   }
   
   /**
    * Verify the integrity of a document revision in a QLDB ledger.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbClient: QLDB = new QLDB();
           const qldbDriver: QldbDriver = getQldbDriver();
   
           const registration = VEHICLE_REGISTRATION[0];
           const vin: string = registration.VIN;
   
           await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
               await verifyRegistration(txn, LEDGER_NAME, vin, qldbClient);
           });
       } catch (e) {
           error(`Unable to verify revision: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```
**注意**  
在`getRevision`函数返回指定文档修订版本的证明后，此程序使用客户端 API 验证该修订版。

1. 要运行编译后的程序，请输入以下命令。

   ```
   node dist/GetRevision.js
   ```

如果您不再需要使用`vehicle-registration`分类账，请继续[第 8 步（可选）：清除资源](getting-started.nodejs.step-8.md)。

# 第 8 步（可选）：清除资源
<a name="getting-started.nodejs.step-8"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

您可以继续使用 `vehicle-registration` 分类账。但是，如果您不再需要，请将其删除。

**删除分类账**

1. 使用以下程序（`DeleteLedger.ts`）以删除您的 `vehicle-registration` 分类账及其所有内容。

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { isResourceNotFoundException } from "amazon-qldb-driver-nodejs";
   import { AWSError, QLDB } from "aws-sdk";
   import { DeleteLedgerRequest, DescribeLedgerRequest } from "aws-sdk/clients/qldb";
   
   import { setDeletionProtection } from "./DeletionProtection";
   import { LEDGER_NAME } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   import { sleep } from "./qldb/Util";
   
   const LEDGER_DELETION_POLL_PERIOD_MS = 20000;
   
   /**
    * Send a request to QLDB to delete the specified ledger.
    * @param ledgerName Name of the ledger to be deleted.
    * @param qldbClient The QLDB control plane client to use.
    * @returns Promise which fulfills with void.
    */
   export async function deleteLedger(ledgerName: string, qldbClient: QLDB): Promise<void> {
       log(`Attempting to delete the ledger with name: ${ledgerName}`);
       const request: DeleteLedgerRequest = {
           Name: ledgerName
       };
       await qldbClient.deleteLedger(request).promise();
       log("Success.");
   }
   
   /**
    * Wait for the ledger to be deleted.
    * @param ledgerName Name of the ledger to be deleted.
    * @param qldbClient The QLDB control plane client to use.
    * @returns Promise which fulfills with void.
    */
   export async function waitForDeleted(ledgerName: string, qldbClient: QLDB): Promise<void> {
       log("Waiting for the ledger to be deleted...");
       const request: DescribeLedgerRequest = {
           Name: ledgerName
       };
       let isDeleted: boolean = false;
       while (true) {
           await qldbClient.describeLedger(request).promise().catch((error: AWSError) => {
               if (isResourceNotFoundException(error)) {
                   isDeleted = true;
                   log("Success. Ledger is deleted.");
               }
           });
           if (isDeleted) {
               break;
           }
           log("The ledger is still being deleted. Please wait...");
           await sleep(LEDGER_DELETION_POLL_PERIOD_MS);
       }
   }
   
   /**
    * Delete a ledger.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbClient: QLDB = new QLDB();
           await setDeletionProtection(LEDGER_NAME, qldbClient, false);
           await deleteLedger(LEDGER_NAME, qldbClient);
           await waitForDeleted(LEDGER_NAME, qldbClient);
       } catch (e) {
           error(`Unable to delete the ledger: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```
**注意**  
如果分类账启用了删除保护，您必须先禁用它，然后才能使用 QLDB API 删除分类账。

1. 要运行编译后的程序，请输入以下命令。

   ```
   node dist/DeleteLedger.js
   ```

# Amazon QLDB Python 教程
<a name="getting-started.python.tutorial"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在本教程示例应用程序的本实现中，您将使用带有 Amazon QLDB 驱动程序来创建 QLDB 账本并使用示例数据填充 适用于 Python (Boto3) 的 AWS SDK 该账本。

在学习本教程时，您可以参考 *AWS 适用于 Python 的 QLDB Driver（Boto3）API 参考* 中的 [QLDB 低级客户端](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/qldb.html) 来管理 API 操作。有关事务数据操作，您可参见[适用于 Python 的 QLDB Driver API 参考](https://amazon-qldb-driver-python.readthedocs.io/en/stable/)。

**注意**  
在适用的情况下，某些用例对于 Python 的 QLDB 驱动程序的每个支持主要版本都配备不同的命令或代码示例。

**Topics**
+ [

# 安装 Amazon QLDB Python 示例应用程序
](sample-app.python.md)
+ [

# 第 1 步：创建新的分类账
](getting-started.python.step-1.md)
+ [

# 第 2 步：测试与分类账的连接
](getting-started.python.step-2.md)
+ [

# 步骤 3：创建表、索引与示例数据
](getting-started.python.step-3.md)
+ [

# 步骤 4：查询分类账中的表
](getting-started.python.step-4.md)
+ [

# 第 5 步：修改分类账中的文档
](getting-started.python.step-5.md)
+ [

# 步骤 6：查看文档修订历史记录
](getting-started.python.step-6.md)
+ [

# 第 7 步：验证分类账中的文档
](getting-started.python.step-7.md)
+ [

# 第 8 步（可选）：清除资源
](getting-started.python.step-8.md)

# 安装 Amazon QLDB Python 示例应用程序
<a name="sample-app.python"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

本节介绍如何为 Python 教程安装和运行所提供的 Amazon QLDB 示例应用程序。 step-by-step此示例应用程序的用例是机动车辆部门（DMV）数据库，用于追踪有关车辆登记的完整历史信息。

适用于 Python 的 DMV 示例应用程序在 [aws-samples/-amazon-qldb-dmv-sample](https://github.com/aws-samples/amazon-qldb-dmv-sample-python) python GitHub 存储库中是开源的。

## 先决条件
<a name="sample-app.python.prereqs"></a>

在开始之前，请确保您已完成适用于 Python [先决条件](getting-started.python.md#getting-started.python.prereqs) 的 QLDB 驱动程序。这包括安装 Python 并执行以下操作：

1. 注册 AWS.

1. 创建具有适当 QLDB 权限的用户。

1. 授权以编程方式访问开发。

要完成本教程中的所有步骤，您需要通过 QLDB API 对分类账资源拥有完全管理权限。

## 安装
<a name="sample-app.python.install"></a>

**要安装示例应用程序**

1. 输入以下`pip`命令以克隆和安装示例应用程序 GitHub。

------
#### [ 3.x ]

   ```
   pip install git+https://github.com/aws-samples/amazon-qldb-dmv-sample-python.git
   ```

------
#### [ 2.x ]

   ```
   pip install git+https://github.com/aws-samples/amazon-qldb-dmv-sample-python.git@v1.0.0
   ```

------

   示例应用程序打包了本教程中的完整源代码及其依赖项，包括 Python 驱动程序和 [适用于 Python (Boto3) 的 AWS SDK](https://aws.amazon.com/sdk-for-python)。

1. 在命令行开始运行代码之前，请将当前工作目录切换到 `pyqldbsamples` 包的安装位置。输入以下命令。

   ```
   cd $(python -c "import pyqldbsamples; print(pyqldbsamples.__path__[0])")
   ```

1. 继续 [第 1 步：创建新的分类账](getting-started.python.step-1.md) 开始教程并创建分类账。

# 第 1 步：创建新的分类账
<a name="getting-started.python.step-1"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在此步骤中，您将创建一个名为 `vehicle-registration` 的新 Amazon QLDB 分类账。

**创建新分类账**

1. 查看以下文件（`constants.py`），其包含本教程中所有其他程序使用的常量值。

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   
   
   class Constants:
       """
       Constant values used throughout this tutorial.
       """
       LEDGER_NAME = "vehicle-registration"
   
       VEHICLE_REGISTRATION_TABLE_NAME = "VehicleRegistration"
       VEHICLE_TABLE_NAME = "Vehicle"
       PERSON_TABLE_NAME = "Person"
       DRIVERS_LICENSE_TABLE_NAME = "DriversLicense"
   
       LICENSE_NUMBER_INDEX_NAME = "LicenseNumber"
       GOV_ID_INDEX_NAME = "GovId"
       VEHICLE_VIN_INDEX_NAME = "VIN"
       LICENSE_PLATE_NUMBER_INDEX_NAME = "LicensePlateNumber"
       PERSON_ID_INDEX_NAME = "PersonId"
   
       JOURNAL_EXPORT_S3_BUCKET_NAME_PREFIX = "qldb-tutorial-journal-export"
       USER_TABLES = "information_schema.user_tables"
       S3_BUCKET_ARN_TEMPLATE = "arn:aws:s3:::"
       LEDGER_NAME_WITH_TAGS = "tags"
   
       RETRY_LIMIT = 4
   ```

1. 使用以下程序（`create_ledger.py`）创建名为 `vehicle-registration` 的分类账。

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   from time import sleep
   
   from boto3 import client
   
   from pyqldbsamples.constants import Constants
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   qldb_client = client('qldb')
   
   LEDGER_CREATION_POLL_PERIOD_SEC = 10
   ACTIVE_STATE = "ACTIVE"
   
   
   def create_ledger(name):
       """
       Create a new ledger with the specified name.
   
       :type name: str
       :param name: Name for the ledger to be created.
   
       :rtype: dict
       :return: Result from the request.
       """
       logger.info("Let's create the ledger named: {}...".format(name))
       result = qldb_client.create_ledger(Name=name, PermissionsMode='ALLOW_ALL')
       logger.info('Success. Ledger state: {}.'.format(result.get('State')))
       return result
   
   
   def wait_for_active(name):
       """
       Wait for the newly created ledger to become active.
   
       :type name: str
       :param name: The ledger to check on.
   
       :rtype: dict
       :return: Result from the request.
       """
       logger.info('Waiting for ledger to become active...')
       while True:
           result = qldb_client.describe_ledger(Name=name)
           if result.get('State') == ACTIVE_STATE:
               logger.info('Success. Ledger is active and ready to use.')
               return result
           logger.info('The ledger is still creating. Please wait...')
           sleep(LEDGER_CREATION_POLL_PERIOD_SEC)
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Create a ledger and wait for it to be active.
       """
       try:
           create_ledger(ledger_name)
           wait_for_active(ledger_name)
       except Exception as e:
           logger.exception('Unable to create the ledger!')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```
**注意**  
在 `create_ledger` 调用中，您必须指定分类账名称和权限模式。我们强烈建议使用 `STANDARD` 权限模式来最大限度地提高分类账数据的安全性。
创建分类账时，将默认启用 *删除保护*。QLDB 中有一项功能，可防止分类账被任何用户删除。您可以选择使用 QLDB API 或 AWS Command Line Interface () 在创建账本时禁用删除保护。AWS CLI
您还可选择指定要附加到分类账的标签。

1. 要运行该程序，请输入以下命令。

   ```
   python create_ledger.py
   ```

要验证您与新分类账的连接，请继续 [第 2 步：测试与分类账的连接](getting-started.python.step-2.md)。

# 第 2 步：测试与分类账的连接
<a name="getting-started.python.step-2"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在此步骤中，您将验证是否可以使用事务数据 API 端点连接至 Amazon QLDB 中的`vehicle-registration`分类账。

**测试与分类账的连接**

1. 查看以下程序（`connect_to_ledger.py`），该程序创建了与 `vehicle-registration` 分类账的数据会话连接。

------
#### [ 3.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from botocore.exceptions import ClientError
   
   from pyqldb.driver.qldb_driver import QldbDriver
   from pyqldbsamples.constants import Constants
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def create_qldb_driver(ledger_name=Constants.LEDGER_NAME, region_name=None, endpoint_url=None, boto3_session=None):
       """
       Create a QLDB driver for executing transactions.
   
       :type ledger_name: str
       :param ledger_name: The QLDB ledger name.
   
       :type region_name: str
       :param region_name: See [1].
   
       :type endpoint_url: str
       :param endpoint_url: See [1].
   
       :type boto3_session: :py:class:`boto3.session.Session`
       :param boto3_session: The boto3 session to create the client with (see [1]).
   
       :rtype: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :return: A QLDB driver object.
   
       [1]: `Boto3 Session.client Reference <https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html#boto3.session.Session.client>`.
       """
       qldb_driver = QldbDriver(ledger_name=ledger_name, region_name=region_name, endpoint_url=endpoint_url,
                                boto3_session=boto3_session)
       return qldb_driver
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Connect to a given ledger using default settings.
       """
       try:
           with create_qldb_driver(ledger_name) as driver:
               logger.info('Listing table names ')
               for table in driver.list_tables():
                   logger.info(table)
       except ClientError as ce:
           logger.exception('Unable to list tables.')
           raise ce
   
   
   if __name__ == '__main__':
       main()
   ```

**注意**  
要在分类账上运行数据事务，必须创建一个 QLDB 驱动对象以连接到指定的分类账。这与您在上一步中创建分类账时使用的 `qldb_client` 客户端对象不同。之前的客户端仅用于[Amazon QLDB API 参考](api-reference.md)中列出的管理 API 操作。
创建此驱动程序对象时，您必须指定分类账名称。

------
#### [ 2.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from botocore.exceptions import ClientError
   
   from pyqldb.driver.pooled_qldb_driver import PooledQldbDriver
   from pyqldbsamples.constants import Constants
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def create_qldb_driver(ledger_name=Constants.LEDGER_NAME, region_name=None, endpoint_url=None, boto3_session=None):
       """
       Create a QLDB driver for creating sessions.
   
       :type ledger_name: str
       :param ledger_name: The QLDB ledger name.
   
       :type region_name: str
       :param region_name: See [1].
   
       :type endpoint_url: str
       :param endpoint_url: See [1].
   
       :type boto3_session: :py:class:`boto3.session.Session`
       :param boto3_session: The boto3 session to create the client with (see [1]).
   
       :rtype: :py:class:`pyqldb.driver.pooled_qldb_driver.PooledQldbDriver`
       :return: A pooled QLDB driver object.
   
       [1]: `Boto3 Session.client Reference <https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html#boto3.session.Session.client>`.
       """
       qldb_driver = PooledQldbDriver(ledger_name=ledger_name, region_name=region_name, endpoint_url=endpoint_url,
                                      boto3_session=boto3_session)
       return qldb_driver
   
   
   def create_qldb_session():
       """
       Retrieve a QLDB session object.
   
       :rtype: :py:class:`pyqldb.session.pooled_qldb_session.PooledQldbSession`
       :return: A pooled QLDB session object.
       """
       qldb_session = pooled_qldb_driver.get_session()
       return qldb_session
   
   
   pooled_qldb_driver = create_qldb_driver()
   
   
   if __name__ == '__main__':
       """
       Connect to a session for a given ledger using default settings.
       """
       try:
           qldb_session = create_qldb_session()
           logger.info('Listing table names ')
           for table in qldb_session.list_tables():
               logger.info(table)
       except ClientError:
           logger.exception('Unable to create session.')
   ```

**注意**  
要在分类账上运行数据事务，必须创建一个 QLDB 驱动程序对象以连接到指定的分类账。这与您在上一步中创建分类账时使用的 `qldb_client` 客户端对象不同。此前的客户端仅用于[Amazon QLDB API 参考](api-reference.md)中列出的管理 API 操作。
首先，创建一个池化的 QLDB 驱动程序对象。创建此驱动程序时，您必须指定分类账名称。
然后，您可以从此池驱动程序对象创建会话。

------

1. 要运行该程序，请输入以下命令。

   ```
   python connect_to_ledger.py
   ```

要在 `vehicle-registration` 分类账中创建表，请继续 [步骤 3：创建表、索引与示例数据](getting-started.python.step-3.md)。

# 步骤 3：创建表、索引与示例数据
<a name="getting-started.python.step-3"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

当您的 Amazon QLDB 分类账处于活动状态且接受连接时，您可开始创建有关车辆、车主和注册信息的数据表。创建表和索引后，可以向其中加载数据。

在此步骤中，您将在`vehicle-registration`分类账中创建四个表格：
+ `VehicleRegistration`
+ `Vehicle`
+ `Person`
+ `DriversLicense`

您还可创建以下索引。


****  

| 表名称 | 字段 | 
| --- | --- | 
| VehicleRegistration | VIN | 
| VehicleRegistration | LicensePlateNumber | 
| Vehicle | VIN | 
| Person | GovId | 
| DriversLicense | LicenseNumber | 
| DriversLicense | PersonId | 

插入示例数据时，首先要在 `Person` 表格中插入文档。然后使用系统分配的、来自每个`Person`文档的`id`填充适当`VehicleRegistration` 和 `DriversLicense`文档中的相应文档。

**提示**  
最佳做法是使用系统分配的 `id` 文档作为外键。虽然您可以定义作为唯一标识符的字段（例如车辆的 VIN），但文档的真正唯一标识符是`id`。字段都包含在文档元数据中，您可以在*提交视图*（系统定义的表格视图）中对其进行查询。  
有关 QLDB 中的视图的更多信息，请参阅[核心概念](ledger-structure.md)。了解有关元数据的更多信息，请参阅 [查询文档元数据](working.metadata.md)。

**创建表和索引**

1. 编译并运行以下程序（`create_table.py`）以创建前面提到的表。

------
#### [ 3.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.connect_to_ledger import create_qldb_driver
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def create_table(driver, table_name):
       """
       Create a table with the specified name.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type table_name: str
       :param table_name: Name of the table to create.
   
       :rtype: int
       :return: The number of changes to the database.
       """
       logger.info("Creating the '{}' table...".format(table_name))
       statement = 'CREATE TABLE {}'.format(table_name)
       cursor = driver.execute_lambda(lambda executor: executor.execute_statement(statement))
       logger.info('{} table created successfully.'.format(table_name))
       return len(list(cursor))
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Create registrations, vehicles, owners, and licenses tables.
       """
       try:
           with create_qldb_driver(ledger_name) as driver:
               create_table(driver, Constants.DRIVERS_LICENSE_TABLE_NAME)
               create_table(driver, Constants.PERSON_TABLE_NAME)
               create_table(driver, Constants.VEHICLE_TABLE_NAME)
               create_table(driver, Constants.VEHICLE_REGISTRATION_TABLE_NAME)
               logger.info('Tables created successfully.')
       except Exception as e:
           logger.exception('Errors creating tables.')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```

------
#### [ 2.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.connect_to_ledger import create_qldb_session
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def create_table(transaction_executor, table_name):
       """
       Create a table with the specified name using an Executor object.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type table_name: str
       :param table_name: Name of the table to create.
   
       :rtype: int
       :return: The number of changes to the database.
       """
       logger.info("Creating the '{}' table...".format(table_name))
       statement = 'CREATE TABLE {}'.format(table_name)
       cursor = transaction_executor.execute_statement(statement)
       logger.info('{} table created successfully.'.format(table_name))
       return len(list(cursor))
   
   
   if __name__ == '__main__':
       """
       Create registrations, vehicles, owners, and licenses tables in a single transaction.
       """
       try:
           with create_qldb_session() as session:
               session.execute_lambda(lambda x: create_table(x, Constants.DRIVERS_LICENSE_TABLE_NAME) and
                                      create_table(x, Constants.PERSON_TABLE_NAME) and
                                      create_table(x, Constants.VEHICLE_TABLE_NAME) and
                                      create_table(x, Constants.VEHICLE_REGISTRATION_TABLE_NAME),
                                      lambda retry_attempt: logger.info('Retrying due to OCC conflict...'))
               logger.info('Tables created successfully.')
       except Exception:
           logger.exception('Errors creating tables.')
   ```

------
**注意**  
该程序演示如何使用 `execute_lambda` 函数。在此示例中，您使用 lambda 表达式在单个事务中运行多个`CREATE TABLE` PartiQL 语句  
该 方法采用隐式启动事务，运行 lambda 中的所有语句，然后自动提交事务。

1. 要运行该程序，请输入以下命令。

   ```
   python create_table.py
   ```

1. 如前所述，编译并运行以下程序（`create_index.py`），以在表上创建索引。

------
#### [ 3.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.connect_to_ledger import create_qldb_driver
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def create_index(driver, table_name, index_attribute):
       """
       Create an index for a particular table.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type table_name: str
       :param table_name: Name of the table to add indexes for.
   
       :type index_attribute: str
       :param index_attribute: Index to create on a single attribute.
   
       :rtype: int
       :return: The number of changes to the database.
       """
       logger.info("Creating index on '{}'...".format(index_attribute))
       statement = 'CREATE INDEX on {} ({})'.format(table_name, index_attribute)
       cursor = driver.execute_lambda(lambda executor: executor.execute_statement(statement))
       return len(list(cursor))
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Create indexes on tables in a particular ledger.
       """
       logger.info('Creating indexes on all tables...')
       try:
           with create_qldb_driver(ledger_name) as driver:
               create_index(driver, Constants.PERSON_TABLE_NAME, Constants.GOV_ID_INDEX_NAME)
               create_index(driver, Constants.VEHICLE_TABLE_NAME, Constants.VEHICLE_VIN_INDEX_NAME)
               create_index(driver, Constants.VEHICLE_REGISTRATION_TABLE_NAME, Constants.LICENSE_PLATE_NUMBER_INDEX_NAME)
               create_index(driver, Constants.VEHICLE_REGISTRATION_TABLE_NAME, Constants.VEHICLE_VIN_INDEX_NAME)
               create_index(driver, Constants.DRIVERS_LICENSE_TABLE_NAME, Constants.PERSON_ID_INDEX_NAME)
               create_index(driver, Constants.DRIVERS_LICENSE_TABLE_NAME, Constants.LICENSE_NUMBER_INDEX_NAME)
               logger.info('Indexes created successfully.')
       except Exception as e:
           logger.exception('Unable to create indexes.')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```

------
#### [ 2.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.connect_to_ledger import create_qldb_session
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def create_index(transaction_executor, table_name, index_attribute):
       """
       Create an index for a particular table.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type table_name: str
       :param table_name: Name of the table to add indexes for.
   
       :type index_attribute: str
       :param index_attribute: Index to create on a single attribute.
   
       :rtype: int
       :return: The number of changes to the database.
       """
       logger.info("Creating index on '{}'...".format(index_attribute))
       statement = 'CREATE INDEX on {} ({})'.format(table_name, index_attribute)
       cursor = transaction_executor.execute_statement(statement)
       return len(list(cursor))
   
   
   if __name__ == '__main__':
       """
       Create indexes on tables in a particular ledger.
       """
       logger.info('Creating indexes on all tables in a single transaction...')
       try:
           with create_qldb_session() as session:
               session.execute_lambda(lambda x: create_index(x, Constants.PERSON_TABLE_NAME,
                                                             Constants.GOV_ID_INDEX_NAME)
                                      and create_index(x, Constants.VEHICLE_TABLE_NAME,
                                                       Constants.VEHICLE_VIN_INDEX_NAME)
                                      and create_index(x, Constants.VEHICLE_REGISTRATION_TABLE_NAME,
                                                       Constants.LICENSE_PLATE_NUMBER_INDEX_NAME)
                                      and create_index(x, Constants.VEHICLE_REGISTRATION_TABLE_NAME,
                                                       Constants.VEHICLE_VIN_INDEX_NAME)
                                      and create_index(x, Constants.DRIVERS_LICENSE_TABLE_NAME,
                                                       Constants.PERSON_ID_INDEX_NAME)
                                      and create_index(x, Constants.DRIVERS_LICENSE_TABLE_NAME,
                                                       Constants.LICENSE_NUMBER_INDEX_NAME),
                                      lambda retry_attempt: logger.info('Retrying due to OCC conflict...'))
               logger.info('Indexes created successfully.')
       except Exception:
           logger.exception('Unable to create indexes.')
   ```

------

1. 要运行该程序，请输入以下命令。

   ```
   python create_index.py
   ```

**将样本数据加载到 表中**

1. 查看以下文件（`sample_data.py`），该文件代表您插入 `vehicle-registration` 表中的示例数据。此文件也从 `amazon.ion` 软件包导出，以提供用于转换、解析和打印 [Amazon Ion](ion.md) 数据的辅助函数。

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   from datetime import datetime
   from decimal import Decimal
   from logging import basicConfig, getLogger, INFO
   
   from amazon.ion.simple_types import IonPyBool, IonPyBytes, IonPyDecimal, IonPyDict, IonPyFloat, IonPyInt, IonPyList, \
       IonPyNull, IonPySymbol, IonPyText, IonPyTimestamp
   from amazon.ion.simpleion import dumps, loads
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   IonValue = (IonPyBool, IonPyBytes, IonPyDecimal, IonPyDict, IonPyFloat, IonPyInt, IonPyList, IonPyNull, IonPySymbol,
               IonPyText, IonPyTimestamp)
   
   
   class SampleData:
       """
       Sample domain objects for use throughout this tutorial.
       """
       DRIVERS_LICENSE = [
           {
               'PersonId': '',
               'LicenseNumber': 'LEWISR261LL',
               'LicenseType': 'Learner',
               'ValidFromDate': datetime(2016, 12, 20),
               'ValidToDate': datetime(2020, 11, 15)
           },
           {
               'PersonId': '',
               'LicenseNumber': 'LOGANB486CG',
               'LicenseType': 'Probationary',
               'ValidFromDate': datetime(2016, 4, 6),
               'ValidToDate': datetime(2020, 11, 15)
           },
           {
               'PersonId': '',
               'LicenseNumber': '744 849 301',
               'LicenseType': 'Full',
               'ValidFromDate': datetime(2017, 12, 6),
               'ValidToDate': datetime(2022, 10, 15)
           },
           {
               'PersonId': '',
               'LicenseNumber': 'P626-168-229-765',
               'LicenseType': 'Learner',
               'ValidFromDate': datetime(2017, 8, 16),
               'ValidToDate': datetime(2021, 11, 15)
           },
           {
               'PersonId': '',
               'LicenseNumber': 'S152-780-97-415-0',
               'LicenseType': 'Probationary',
               'ValidFromDate': datetime(2015, 8, 15),
               'ValidToDate': datetime(2021, 8, 21)
           }
       ]
       PERSON = [
           {
               'FirstName': 'Raul',
               'LastName': 'Lewis',
               'Address': '1719 University Street, Seattle, WA, 98109',
               'DOB': datetime(1963, 8, 19),
               'GovId': 'LEWISR261LL',
               'GovIdType': 'Driver License'
           },
           {
               'FirstName': 'Brent',
               'LastName': 'Logan',
               'DOB': datetime(1967, 7, 3),
               'Address': '43 Stockert Hollow Road, Everett, WA, 98203',
               'GovId': 'LOGANB486CG',
               'GovIdType': 'Driver License'
           },
           {
               'FirstName': 'Alexis',
               'LastName': 'Pena',
               'DOB': datetime(1974, 2, 10),
               'Address': '4058 Melrose Street, Spokane Valley, WA, 99206',
               'GovId': '744 849 301',
               'GovIdType': 'SSN'
           },
           {
               'FirstName': 'Melvin',
               'LastName': 'Parker',
               'DOB': datetime(1976, 5, 22),
               'Address': '4362 Ryder Avenue, Seattle, WA, 98101',
               'GovId': 'P626-168-229-765',
               'GovIdType': 'Passport'
           },
           {
               'FirstName': 'Salvatore',
               'LastName': 'Spencer',
               'DOB': datetime(1997, 11, 15),
               'Address': '4450 Honeysuckle Lane, Seattle, WA, 98101',
               'GovId': 'S152-780-97-415-0',
               'GovIdType': 'Passport'
           }
       ]
       VEHICLE = [
           {
               'VIN': '1N4AL11D75C109151',
               'Type': 'Sedan',
               'Year': 2011,
               'Make': 'Audi',
               'Model': 'A5',
               'Color': 'Silver'
           },
           {
               'VIN': 'KM8SRDHF6EU074761',
               'Type': 'Sedan',
               'Year': 2015,
               'Make': 'Tesla',
               'Model': 'Model S',
               'Color': 'Blue'
           },
           {
               'VIN': '3HGGK5G53FM761765',
               'Type': 'Motorcycle',
               'Year': 2011,
               'Make': 'Ducati',
               'Model': 'Monster 1200',
               'Color': 'Yellow'
           },
           {
               'VIN': '1HVBBAANXWH544237',
               'Type': 'Semi',
               'Year': 2009,
               'Make': 'Ford',
               'Model': 'F 150',
               'Color': 'Black'
           },
           {
               'VIN': '1C4RJFAG0FC625797',
               'Type': 'Sedan',
               'Year': 2019,
               'Make': 'Mercedes',
               'Model': 'CLK 350',
               'Color': 'White'
           }
       ]
       VEHICLE_REGISTRATION = [
           {
               'VIN': '1N4AL11D75C109151',
               'LicensePlateNumber': 'LEWISR261LL',
               'State': 'WA',
               'City': 'Seattle',
               'ValidFromDate': datetime(2017, 8, 21),
               'ValidToDate': datetime(2020, 5, 11),
               'PendingPenaltyTicketAmount': Decimal('90.25'),
               'Owners': {
                   'PrimaryOwner': {'PersonId': ''},
                   'SecondaryOwners': []
               }
           },
           {
               'VIN': 'KM8SRDHF6EU074761',
               'LicensePlateNumber': 'CA762X',
               'State': 'WA',
               'City': 'Kent',
               'PendingPenaltyTicketAmount': Decimal('130.75'),
               'ValidFromDate': datetime(2017, 9, 14),
               'ValidToDate': datetime(2020, 6, 25),
               'Owners': {
                   'PrimaryOwner': {'PersonId': ''},
                   'SecondaryOwners': []
               }
           },
           {
               'VIN': '3HGGK5G53FM761765',
               'LicensePlateNumber': 'CD820Z',
               'State': 'WA',
               'City': 'Everett',
               'PendingPenaltyTicketAmount': Decimal('442.30'),
               'ValidFromDate': datetime(2011, 3, 17),
               'ValidToDate': datetime(2021, 3, 24),
               'Owners': {
                   'PrimaryOwner': {'PersonId': ''},
                   'SecondaryOwners': []
               }
           },
           {
               'VIN': '1HVBBAANXWH544237',
               'LicensePlateNumber': 'LS477D',
               'State': 'WA',
               'City': 'Tacoma',
               'PendingPenaltyTicketAmount': Decimal('42.20'),
               'ValidFromDate': datetime(2011, 10, 26),
               'ValidToDate': datetime(2023, 9, 25),
               'Owners': {
                   'PrimaryOwner': {'PersonId': ''},
                   'SecondaryOwners': []
               }
           },
           {
               'VIN': '1C4RJFAG0FC625797',
               'LicensePlateNumber': 'TH393F',
               'State': 'WA',
               'City': 'Olympia',
               'PendingPenaltyTicketAmount': Decimal('30.45'),
               'ValidFromDate': datetime(2013, 9, 2),
               'ValidToDate': datetime(2024, 3, 19),
               'Owners': {
                   'PrimaryOwner': {'PersonId': ''},
                   'SecondaryOwners': []
               }
           }
       ]
   
   
   def convert_object_to_ion(py_object):
       """
       Convert a Python object into an Ion object.
   
       :type py_object: object
       :param py_object: The object to convert.
   
       :rtype: :py:class:`amazon.ion.simple_types.IonPyValue`
       :return: The converted Ion object.
       """
       ion_object = loads(dumps(py_object))
       return ion_object
   
   
   def to_ion_struct(key, value):
       """
       Convert the given key and value into an Ion struct.
   
       :type key: str
       :param key: The key which serves as an unique identifier.
   
       :type value: str
       :param value: The value associated with a given key.
   
       :rtype: :py:class:`amazon.ion.simple_types.IonPyDict`
       :return: The Ion dictionary object.
       """
       ion_struct = dict()
       ion_struct[key] = value
       return loads(str(ion_struct))
   
   
   def get_document_ids(transaction_executor, table_name, field, value):
       """
       Gets the document IDs from the given table.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type table_name: str
       :param table_name: The table name to query.
   
       :type field: str
       :param field: A field to query.
   
       :type value: str
       :param value: The key of the given field.
   
       :rtype: list
       :return: A list of document IDs.
       """
       query = "SELECT id FROM {} AS t BY id WHERE t.{} = ?".format(table_name, field)
       cursor = transaction_executor.execute_statement(query, convert_object_to_ion(value))
       return list(map(lambda table: table.get('id'), cursor))
   
   
   def get_document_ids_from_dml_results(result):
       """
       Return a list of modified document IDs as strings from DML results.
   
       :type result: :py:class:`pyqldb.cursor.buffered_cursor.BufferedCursor`
       :param: result: The result set from DML operation.
   
       :rtype: list
       :return: List of document IDs.
       """
       ret_val = list(map(lambda x: x.get('documentId'), result))
       return ret_val
   
   
   def print_result(cursor):
       """
       Pretty print the result set. Returns the number of documents in the result set.
   
       :type cursor: :py:class:`pyqldb.cursor.stream_cursor.StreamCursor`/
                     :py:class:`pyqldb.cursor.buffered_cursor.BufferedCursor`
       :param cursor: An instance of the StreamCursor or BufferedCursor class.
   
       :rtype: int
       :return: Number of documents in the result set.
       """
       result_counter = 0
       for row in cursor:
           # Each row would be in Ion format.
           print_ion(row)
           result_counter += 1
       return result_counter
   
   
   def print_ion(ion_value):
       """
       Pretty print an Ion Value.
   
       :type ion_value: :py:class:`amazon.ion.simple_types.IonPySymbol`
       :param ion_value: Any Ion Value to be pretty printed.
       """
       logger.info(dumps(ion_value, binary=False, indent='  ', omit_version_marker=True))
   ```
**注意**  
该`get_document_ids`函数运行一个查询， IDs 从表中返回系统分配的文档。要了解更多信息，请参阅 [通过 BY 子句查询文档 ID](working.metadata.by-clause.md)。

1. 使用以下程序（`insert_document.py`），将示例数据插入表内。

------
#### [ 3.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.model.sample_data import convert_object_to_ion, SampleData, get_document_ids_from_dml_results
   from pyqldbsamples.connect_to_ledger import create_qldb_driver
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def update_person_id(document_ids):
       """
       Update the PersonId value for DriversLicense records and the PrimaryOwner value for VehicleRegistration records.
   
       :type document_ids: list
       :param document_ids: List of document IDs.
   
       :rtype: list
       :return: Lists of updated DriversLicense records and updated VehicleRegistration records.
       """
       new_drivers_licenses = SampleData.DRIVERS_LICENSE.copy()
       new_vehicle_registrations = SampleData.VEHICLE_REGISTRATION.copy()
       for i in range(len(SampleData.PERSON)):
           drivers_license = new_drivers_licenses[i]
           registration = new_vehicle_registrations[i]
           drivers_license.update({'PersonId': str(document_ids[i])})
           registration['Owners']['PrimaryOwner'].update({'PersonId': str(document_ids[i])})
       return new_drivers_licenses, new_vehicle_registrations
   
   
   def insert_documents(driver, table_name, documents):
       """
       Insert the given list of documents into a table in a single transaction.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type table_name: str
       :param table_name: Name of the table to insert documents into.
   
       :type documents: list
       :param documents: List of documents to insert.
   
       :rtype: list
       :return: List of documents IDs for the newly inserted documents.
       """
       logger.info('Inserting some documents in the {} table...'.format(table_name))
       statement = 'INSERT INTO {} ?'.format(table_name)
       cursor = driver.execute_lambda(lambda executor: executor.execute_statement(statement,
                                                                                  convert_object_to_ion(documents)))
       list_of_document_ids = get_document_ids_from_dml_results(cursor)
   
       return list_of_document_ids
   
   
   def update_and_insert_documents(driver):
       """
       Handle the insertion of documents and updating PersonIds.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
       """
       list_ids = insert_documents(driver, Constants.PERSON_TABLE_NAME, SampleData.PERSON)
   
       logger.info("Updating PersonIds for 'DriversLicense' and PrimaryOwner for 'VehicleRegistration'...")
       new_licenses, new_registrations = update_person_id(list_ids)
   
       insert_documents(driver, Constants.VEHICLE_TABLE_NAME, SampleData.VEHICLE)
       insert_documents(driver, Constants.VEHICLE_REGISTRATION_TABLE_NAME, new_registrations)
       insert_documents(driver, Constants.DRIVERS_LICENSE_TABLE_NAME, new_licenses)
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Insert documents into a table in a QLDB ledger.
       """
       try:
           with create_qldb_driver(ledger_name) as driver:
               # An INSERT statement creates the initial revision of a document with a version number of zero.
               # QLDB also assigns a unique document identifier in GUID format as part of the metadata.
               update_and_insert_documents(driver)
               logger.info('Documents inserted successfully!')
       except Exception as e:
           logger.exception('Error inserting or updating documents.')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```

------
#### [ 2.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.model.sample_data import convert_object_to_ion, SampleData, get_document_ids_from_dml_results
   from pyqldbsamples.connect_to_ledger import create_qldb_session
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def update_person_id(document_ids):
       """
       Update the PersonId value for DriversLicense records and the PrimaryOwner value for VehicleRegistration records.
   
       :type document_ids: list
       :param document_ids: List of document IDs.
   
       :rtype: list
       :return: Lists of updated DriversLicense records and updated VehicleRegistration records.
       """
       new_drivers_licenses = SampleData.DRIVERS_LICENSE.copy()
       new_vehicle_registrations = SampleData.VEHICLE_REGISTRATION.copy()
       for i in range(len(SampleData.PERSON)):
           drivers_license = new_drivers_licenses[i]
           registration = new_vehicle_registrations[i]
           drivers_license.update({'PersonId': str(document_ids[i])})
           registration['Owners']['PrimaryOwner'].update({'PersonId': str(document_ids[i])})
       return new_drivers_licenses, new_vehicle_registrations
   
   
   def insert_documents(transaction_executor, table_name, documents):
       """
       Insert the given list of documents into a table in a single transaction.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type table_name: str
       :param table_name: Name of the table to insert documents into.
   
       :type documents: list
       :param documents: List of documents to insert.
   
       :rtype: list
       :return: List of documents IDs for the newly inserted documents.
       """
       logger.info('Inserting some documents in the {} table...'.format(table_name))
       statement = 'INSERT INTO {} ?'.format(table_name)
       cursor = transaction_executor.execute_statement(statement, convert_object_to_ion(documents))
       list_of_document_ids = get_document_ids_from_dml_results(cursor)
   
       return list_of_document_ids
   
   
   def update_and_insert_documents(transaction_executor):
       """
       Handle the insertion of documents and updating PersonIds all in a single transaction.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
       """
       list_ids = insert_documents(transaction_executor, Constants.PERSON_TABLE_NAME, SampleData.PERSON)
   
       logger.info("Updating PersonIds for 'DriversLicense' and PrimaryOwner for 'VehicleRegistration'...")
       new_licenses, new_registrations = update_person_id(list_ids)
   
       insert_documents(transaction_executor, Constants.VEHICLE_TABLE_NAME, SampleData.VEHICLE)
       insert_documents(transaction_executor, Constants.VEHICLE_REGISTRATION_TABLE_NAME, new_registrations)
       insert_documents(transaction_executor, Constants.DRIVERS_LICENSE_TABLE_NAME, new_licenses)
   
   
   if __name__ == '__main__':
       """
       Insert documents into a table in a QLDB ledger.
       """
       try:
           with create_qldb_session() as session:
               # An INSERT statement creates the initial revision of a document with a version number of zero.
               # QLDB also assigns a unique document identifier in GUID format as part of the metadata.
               session.execute_lambda(lambda executor: update_and_insert_documents(executor),
                                      lambda retry_attempt: logger.info('Retrying due to OCC conflict...'))
               logger.info('Documents inserted successfully!')
       except Exception:
           logger.exception('Error inserting or updating documents.')
   ```

------
**注意**  
该程序演示如何使用参数化值调用`execute_statement`方法。除了要运行的 PartiQL 语句之外，您还可以传递数据参数。在语句字符串中将问号（`?`）作为变量占位符。
如果 `INSERT` 语句成功，则返回每个插入文档的`id`。

1. 要运行该程序，请输入以下命令。

   ```
   python insert_document.py
   ```

接下来，您可以使用 `SELECT` 语句从 `vehicle-registration` 分类账中的表中读取数据。继续执行[步骤 4：查询分类账中的表](getting-started.python.step-4.md)。

# 步骤 4：查询分类账中的表
<a name="getting-started.python.step-4"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在 Amazon QLDB 分类账中创建表格和加载数据后，您可运行查询以查看刚刚插入的车辆登记数据。QLDB 使用 [PartiQL](ql-reference.md)作为其查询语言，使用 [Amazon Ion](ion.md)作为其面向文档的数据模型。

PartiQL 是开源、与 SQL 兼容的查询语言，现已扩展为可与 Ion 配合使用。使用 PartiQL，您可使用熟悉的 SQL 运算符插入、查询和管理数据。Amazon Ion 是 JSON 的超集。Ion 是基于文档的开源数据格式，可让您灵活地存储和处理结构化、半结构化和嵌套数据。

在此步骤中，您将使用 `SELECT` 语句从 `vehicle-registration`分类账中的表中读取数据。

**警告**  
当您在没有索引查找的情况下运行查询时，它会调用全表扫描。PartiQL 之所以支持此类查询，是因为其与 SQL 兼容。但是，*切勿*在 QLDB 中对生产用例运行表扫描。表扫描可能会导致大型表出现性能问题，包括并发冲突与事务超时。  
为避免表扫描，必须在索引字段或文档 ID 上使用*相等*运算符（`WHERE indexedField = 123`或`WHERE indexedField IN (456, 789)`）运行带有`WHERE`谓词子句的语句。有关更多信息，请参阅 [优化查询性能](working.optimize.md)。

**查询表格**

1. 使用以下程序（`find_vehicles.py`），以查询分类账中某人名下注册的所有车辆。

------
#### [ 3.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.model.sample_data import get_document_ids, print_result, SampleData
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.connect_to_ledger import create_qldb_driver
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def find_vehicles_for_owner(driver, gov_id):
       """
       Find vehicles registered under a driver using their government ID.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type gov_id: str
       :param gov_id: The owner's government ID.
       """
       document_ids = driver.execute_lambda(lambda executor: get_document_ids(executor, Constants.PERSON_TABLE_NAME,
                                                                              'GovId', gov_id))
   
       query = "SELECT Vehicle FROM Vehicle INNER JOIN VehicleRegistration AS r " \
               "ON Vehicle.VIN = r.VIN WHERE r.Owners.PrimaryOwner.PersonId = ?"
   
       for ids in document_ids:
           cursor = driver.execute_lambda(lambda executor: executor.execute_statement(query, ids))
           logger.info('List of Vehicles for owner with GovId: {}...'.format(gov_id))
           print_result(cursor)
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Find all vehicles registered under a person.
       """
       try:
           with create_qldb_driver(ledger_name) as driver:
               # Find all vehicles registered under a person.
               gov_id = SampleData.PERSON[0]['GovId']
               find_vehicles_for_owner(driver, gov_id)
       except Exception as e:
           logger.exception('Error getting vehicles for owner.')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```

------
#### [ 2.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.model.sample_data import get_document_ids, print_result, SampleData
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.connect_to_ledger import create_qldb_session
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def find_vehicles_for_owner(transaction_executor, gov_id):
       """
       Find vehicles registered under a driver using their government ID.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type gov_id: str
       :param gov_id: The owner's government ID.
       """
       document_ids = get_document_ids(transaction_executor, Constants.PERSON_TABLE_NAME, 'GovId', gov_id)
   
       query = "SELECT Vehicle FROM Vehicle INNER JOIN VehicleRegistration AS r " \
               "ON Vehicle.VIN = r.VIN WHERE r.Owners.PrimaryOwner.PersonId = ?"
   
       for ids in document_ids:
           cursor = transaction_executor.execute_statement(query, ids)
           logger.info('List of Vehicles for owner with GovId: {}...'.format(gov_id))
           print_result(cursor)
   
   
   if __name__ == '__main__':
       """
       Find all vehicles registered under a person.
       """
       try:
           with create_qldb_session() as session:
               # Find all vehicles registered under a person.
               gov_id = SampleData.PERSON[0]['GovId']
               session.execute_lambda(lambda executor: find_vehicles_for_owner(executor, gov_id),
                                      lambda retry_attempt: logger.info('Retrying due to OCC conflict...'))
       except Exception:
           logger.exception('Error getting vehicles for owner.')
   ```

------
**注意**  
首先，该程序在 `Person` 表格中查询文档`GovId LEWISR261LL`，以获取其`id`元数据字段。  
然后，它使用文档`id`，通过`PrimaryOwner.PersonId`作为外键查询`VehicleRegistration`表。它还结合`VIN`字段上的`Vehicle`表`VehicleRegistration`。

1. 要运行该程序，请输入以下命令。

   ```
   python find_vehicles.py
   ```

要了解如何修改 `vehicle-registration` 分类账表格中的文档，请参阅 [第 5 步：修改分类账中的文档](getting-started.python.step-5.md)。

# 第 5 步：修改分类账中的文档
<a name="getting-started.python.step-5"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

现在您有了要处理的数据，可以开始在 Amazon QLDB 中对 `vehicle-registration` 分类账文档进行更改。在此步骤中，以下代码示例介绍了如何运行数据操作语言（DML）语句。这些声明更新一辆车的主要车主，并为另一辆车添加了次要车主。

**修改文档**

1. 使用以下程序（`transfer_vehicle_ownership.py`），更新分类账中带有 VIN `1N4AL11D75C109151` 的车辆的主要车主。

------
#### [ 3.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.add_secondary_owner import get_document_ids, print_result, SampleData
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.model.sample_data import convert_object_to_ion
   from pyqldbsamples.connect_to_ledger import create_qldb_driver
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def find_person_from_document_id(transaction_executor, document_id):
       """
       Query a driver's information using the given ID.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type document_id: :py:class:`amazon.ion.simple_types.IonPyText`
       :param document_id: The document ID required to query for the person.
   
       :rtype: :py:class:`amazon.ion.simple_types.IonPyDict`
       :return: The resulting document from the query.
       """
       query = 'SELECT p.* FROM Person AS p BY pid WHERE pid = ?'
       cursor = transaction_executor.execute_statement(query, document_id)
       return next(cursor)
   
   
   def find_primary_owner_for_vehicle(driver, vin):
       """
       Find the primary owner of a vehicle given its VIN.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type vin: str
       :param vin: The VIN to find primary owner for.
   
       :rtype: :py:class:`amazon.ion.simple_types.IonPyDict`
       :return: The resulting document from the query.
       """
       logger.info('Finding primary owner for vehicle with VIN: {}.'.format(vin))
       query = "SELECT Owners.PrimaryOwner.PersonId FROM VehicleRegistration AS v WHERE v.VIN = ?"
       cursor = driver.execute_lambda(lambda executor: executor.execute_statement(query, convert_object_to_ion(vin)))
       try:
           return driver.execute_lambda(lambda executor: find_person_from_document_id(executor,
                                                                                      next(cursor).get('PersonId')))
       except StopIteration:
           logger.error('No primary owner registered for this vehicle.')
           return None
   
   
   def update_vehicle_registration(driver, vin, document_id):
       """
       Update the primary owner for a vehicle using the given VIN.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type vin: str
       :param vin: The VIN for the vehicle to operate on.
   
       :type document_id: :py:class:`amazon.ion.simple_types.IonPyText`
       :param document_id: New PersonId for the primary owner.
   
       :raises RuntimeError: If no vehicle registration was found using the given document ID and VIN.
       """
       logger.info('Updating the primary owner for vehicle with Vin: {}...'.format(vin))
       statement = "UPDATE VehicleRegistration AS r SET r.Owners.PrimaryOwner.PersonId = ? WHERE r.VIN = ?"
       cursor = driver.execute_lambda(lambda executor: executor.execute_statement(statement, document_id,
                                                                                  convert_object_to_ion(vin)))
       try:
           print_result(cursor)
           logger.info('Successfully transferred vehicle with VIN: {} to new owner.'.format(vin))
       except StopIteration:
           raise RuntimeError('Unable to transfer vehicle, could not find registration.')
   
   
   def validate_and_update_registration(driver, vin, current_owner, new_owner):
       """
       Validate the current owner of the given vehicle and transfer its ownership to a new owner.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type vin: str
       :param vin: The VIN of the vehicle to transfer ownership of.
   
       :type current_owner: str
       :param current_owner: The GovId of the current owner of the vehicle.
   
       :type new_owner: str
       :param new_owner: The GovId of the new owner of the vehicle.
   
       :raises RuntimeError: If unable to verify primary owner.
       """
       primary_owner = find_primary_owner_for_vehicle(driver, vin)
       if primary_owner is None or primary_owner['GovId'] != current_owner:
           raise RuntimeError('Incorrect primary owner identified for vehicle, unable to transfer.')
   
       document_ids = driver.execute_lambda(lambda executor: get_document_ids(executor, Constants.PERSON_TABLE_NAME,
                                                                              'GovId', new_owner))
       update_vehicle_registration(driver, vin, document_ids[0])
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Find primary owner for a particular vehicle's VIN.
       Transfer to another primary owner for a particular vehicle's VIN.
       """
       vehicle_vin = SampleData.VEHICLE[0]['VIN']
       previous_owner = SampleData.PERSON[0]['GovId']
       new_owner = SampleData.PERSON[1]['GovId']
   
       try:
           with create_qldb_driver(ledger_name) as driver:
               validate_and_update_registration(driver, vehicle_vin, previous_owner, new_owner)
       except Exception as e:
           logger.exception('Error updating VehicleRegistration.')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```

------
#### [ 2.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.add_secondary_owner import get_document_ids, print_result, SampleData
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.model.sample_data import convert_object_to_ion
   from pyqldbsamples.connect_to_ledger import create_qldb_session
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def find_person_from_document_id(transaction_executor, document_id):
       """
       Query a driver's information using the given ID.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type document_id: :py:class:`amazon.ion.simple_types.IonPyText`
       :param document_id: The document ID required to query for the person.
   
       :rtype: :py:class:`amazon.ion.simple_types.IonPyDict`
       :return: The resulting document from the query.
       """
       query = 'SELECT p.* FROM Person AS p BY pid WHERE pid = ?'
       cursor = transaction_executor.execute_statement(query, document_id)
       return next(cursor)
   
   
   def find_primary_owner_for_vehicle(transaction_executor, vin):
       """
       Find the primary owner of a vehicle given its VIN.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type vin: str
       :param vin: The VIN to find primary owner for.
   
       :rtype: :py:class:`amazon.ion.simple_types.IonPyDict`
       :return: The resulting document from the query.
       """
       logger.info('Finding primary owner for vehicle with VIN: {}.'.format(vin))
       query = "SELECT Owners.PrimaryOwner.PersonId FROM VehicleRegistration AS v WHERE v.VIN = ?"
       cursor = transaction_executor.execute_statement(query, convert_object_to_ion(vin))
       try:
           return find_person_from_document_id(transaction_executor, next(cursor).get('PersonId'))
       except StopIteration:
           logger.error('No primary owner registered for this vehicle.')
           return None
   
   
   def update_vehicle_registration(transaction_executor, vin, document_id):
       """
       Update the primary owner for a vehicle using the given VIN.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type vin: str
       :param vin: The VIN for the vehicle to operate on.
   
       :type document_id: :py:class:`amazon.ion.simple_types.IonPyText`
       :param document_id: New PersonId for the primary owner.
   
       :raises RuntimeError: If no vehicle registration was found using the given document ID and VIN.
       """
       logger.info('Updating the primary owner for vehicle with Vin: {}...'.format(vin))
       statement = "UPDATE VehicleRegistration AS r SET r.Owners.PrimaryOwner.PersonId = ? WHERE r.VIN = ?"
       cursor = transaction_executor.execute_statement(statement, document_id, convert_object_to_ion(vin))
       try:
           print_result(cursor)
           logger.info('Successfully transferred vehicle with VIN: {} to new owner.'.format(vin))
       except StopIteration:
           raise RuntimeError('Unable to transfer vehicle, could not find registration.')
   
   
   def validate_and_update_registration(transaction_executor, vin, current_owner, new_owner):
       """
       Validate the current owner of the given vehicle and transfer its ownership to a new owner in a single transaction.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type vin: str
       :param vin: The VIN of the vehicle to transfer ownership of.
   
       :type current_owner: str
       :param current_owner: The GovId of the current owner of the vehicle.
   
       :type new_owner: str
       :param new_owner: The GovId of the new owner of the vehicle.
   
       :raises RuntimeError: If unable to verify primary owner.
       """
       primary_owner = find_primary_owner_for_vehicle(transaction_executor, vin)
       if primary_owner is None or primary_owner['GovId'] != current_owner:
           raise RuntimeError('Incorrect primary owner identified for vehicle, unable to transfer.')
   
       document_id = next(get_document_ids(transaction_executor, Constants.PERSON_TABLE_NAME, 'GovId', new_owner))
   
       update_vehicle_registration(transaction_executor, vin, document_id)
   
   
   if __name__ == '__main__':
       """
       Find primary owner for a particular vehicle's VIN.
       Transfer to another primary owner for a particular vehicle's VIN.
       """
       vehicle_vin = SampleData.VEHICLE[0]['VIN']
       previous_owner = SampleData.PERSON[0]['GovId']
       new_owner = SampleData.PERSON[1]['GovId']
   
       try:
           with create_qldb_session() as session:
               session.execute_lambda(lambda executor: validate_and_update_registration(executor, vehicle_vin,
                                                                                        previous_owner, new_owner),
                                      retry_indicator=lambda retry_attempt: logger.info('Retrying due to OCC conflict...'))
       except Exception:
           logger.exception('Error updating VehicleRegistration.')
   ```

------

1. 要运行该程序，请输入以下命令。

   ```
   python transfer_vehicle_ownership.py
   ```

1. 使用以下程序（`add_secondary_owner.py`），将二级车主添加至分类账中带有 VIN `KM8SRDHF6EU074761` 的车辆中。

------
#### [ 3.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.model.sample_data import to_ion_struct, get_document_ids, print_result, SampleData, \
       convert_object_to_ion
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.connect_to_ledger import create_qldb_driver
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def get_document_id_by_gov_id(driver, government_id):
       """
       Find a driver's person ID using the given government ID.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type government_id: str
       :param government_id: A driver's government ID.
   
       :rtype: list
       :return: A list of document IDs.
       """
       logger.info("Finding secondary owner's person ID using given government ID: {}.".format(government_id))
       return driver.execute_lambda(lambda executor: get_document_ids(executor, Constants.PERSON_TABLE_NAME, 'GovId',
                                                                      government_id))
   
   
   def is_secondary_owner_for_vehicle(driver, vin, secondary_owner_id):
       """
       Check whether a secondary owner has already been registered for the given VIN.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type vin: str
       :param vin: VIN of the vehicle to query.
   
       :type secondary_owner_id: str
       :param secondary_owner_id: The secondary owner's person ID.
   
       :rtype: bool
       :return: If the driver has already been registered.
       """
       logger.info('Finding secondary owners for vehicle with VIN: {}...'.format(vin))
       query = 'SELECT Owners.SecondaryOwners FROM VehicleRegistration AS v WHERE v.VIN = ?'
       rows = driver.execute_lambda(lambda executor: executor.execute_statement(query, convert_object_to_ion(vin)))
   
       for row in rows:
           secondary_owners = row.get('SecondaryOwners')
           person_ids = map(lambda owner: owner.get('PersonId').text, secondary_owners)
           if secondary_owner_id in person_ids:
               return True
       return False
   
   
   def add_secondary_owner_for_vin(driver, vin, parameter):
       """
       Add a secondary owner into `VehicleRegistration` table for a particular VIN.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type vin: str
       :param vin: VIN of the vehicle to add a secondary owner for.
   
       :type parameter: :py:class:`amazon.ion.simple_types.IonPyValue`
       :param parameter: The Ion value or Python native type that is convertible to Ion for filling in parameters of the
                         statement.
       """
       logger.info('Inserting secondary owner for vehicle with VIN: {}...'.format(vin))
       statement = "FROM VehicleRegistration AS v WHERE v.VIN = ? INSERT INTO v.Owners.SecondaryOwners VALUE ?"
   
       cursor = driver.execute_lambda(lambda executor: executor.execute_statement(statement, convert_object_to_ion(vin),
                                                                                  parameter))
       logger.info('VehicleRegistration Document IDs which had secondary owners added: ')
       print_result(cursor)
   
   
   def register_secondary_owner(driver, vin, gov_id):
       """
       Register a secondary owner for a vehicle if they are not already registered.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type vin: str
       :param vin: VIN of the vehicle to register a secondary owner for.
   
       :type gov_id: str
       :param gov_id: The government ID of the owner.
       """
       logger.info('Finding the secondary owners for vehicle with VIN: {}.'.format(vin))
   
       document_ids = get_document_id_by_gov_id(driver, gov_id)
   
       for document_id in document_ids:
           if is_secondary_owner_for_vehicle(driver, vin, document_id):
               logger.info('Person with ID {} has already been added as a secondary owner of this vehicle.'.format(gov_id))
           else:
               add_secondary_owner_for_vin(driver, vin, to_ion_struct('PersonId', document_id))
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Finds and adds secondary owners for a vehicle.
       """
       vin = SampleData.VEHICLE[1]['VIN']
       gov_id = SampleData.PERSON[0]['GovId']
       try:
           with create_qldb_driver(ledger_name) as driver:
               register_secondary_owner(driver, vin, gov_id)
               logger.info('Secondary owners successfully updated.')
       except Exception as e:
           logger.exception('Error adding secondary owner.')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```

------
#### [ 2.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.model.sample_data import to_ion_struct, get_document_ids, print_result, SampleData, \
       convert_object_to_ion
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.connect_to_ledger import create_qldb_session
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def get_document_id_by_gov_id(transaction_executor, government_id):
       """
       Find a driver's person ID using the given government ID.
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
       :type government_id: str
       :param government_id: A driver's government ID.
       :rtype: list
       :return: A list of document IDs.
       """
       logger.info("Finding secondary owner's person ID using given government ID: {}.".format(government_id))
       return get_document_ids(transaction_executor, Constants.PERSON_TABLE_NAME, 'GovId', government_id)
   
   
   def is_secondary_owner_for_vehicle(transaction_executor, vin, secondary_owner_id):
       """
       Check whether a secondary owner has already been registered for the given VIN.
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
       :type vin: str
       :param vin: VIN of the vehicle to query.
       :type secondary_owner_id: str
       :param secondary_owner_id: The secondary owner's person ID.
       :rtype: bool
       :return: If the driver has already been registered.
       """
       logger.info('Finding secondary owners for vehicle with VIN: {}...'.format(vin))
       query = 'SELECT Owners.SecondaryOwners FROM VehicleRegistration AS v WHERE v.VIN = ?'
       rows = transaction_executor.execute_statement(query, convert_object_to_ion(vin))
   
       for row in rows:
           secondary_owners = row.get('SecondaryOwners')
           person_ids = map(lambda owner: owner.get('PersonId').text, secondary_owners)
           if secondary_owner_id in person_ids:
               return True
       return False
   
   
   def add_secondary_owner_for_vin(transaction_executor, vin, parameter):
       """
       Add a secondary owner into `VehicleRegistration` table for a particular VIN.
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
       :type vin: str
       :param vin: VIN of the vehicle to add a secondary owner for.
       :type parameter: :py:class:`amazon.ion.simple_types.IonPyValue`
       :param parameter: The Ion value or Python native type that is convertible to Ion for filling in parameters of the
                         statement.
       """
       logger.info('Inserting secondary owner for vehicle with VIN: {}...'.format(vin))
       statement = "FROM VehicleRegistration AS v WHERE v.VIN = '{}' INSERT INTO v.Owners.SecondaryOwners VALUE ?"\
           .format(vin)
   
       cursor = transaction_executor.execute_statement(statement, parameter)
       logger.info('VehicleRegistration Document IDs which had secondary owners added: ')
       print_result(cursor)
   
   
   def register_secondary_owner(transaction_executor, vin, gov_id):
       """
       Register a secondary owner for a vehicle if they are not already registered.
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
       :type vin: str
       :param vin: VIN of the vehicle to register a secondary owner for.
       :type gov_id: str
       :param gov_id: The government ID of the owner.
       """
       logger.info('Finding the secondary owners for vehicle with VIN: {}.'.format(vin))
       document_ids = get_document_id_by_gov_id(transaction_executor, gov_id)
   
       for document_id in document_ids:
           if is_secondary_owner_for_vehicle(transaction_executor, vin, document_id):
               logger.info('Person with ID {} has already been added as a secondary owner of this vehicle.'.format(gov_id))
           else:
               add_secondary_owner_for_vin(transaction_executor, vin, to_ion_struct('PersonId', document_id))
   
   
   if __name__ == '__main__':
       """
       Finds and adds secondary owners for a vehicle.
       """
       vin = SampleData.VEHICLE[1]['VIN']
       gov_id = SampleData.PERSON[0]['GovId']
       try:
           with create_qldb_session() as session:
               session.execute_lambda(lambda executor: register_secondary_owner(executor, vin, gov_id),
                                      lambda retry_attempt: logger.info('Retrying due to OCC conflict...'))
               logger.info('Secondary owners successfully updated.')
       except Exception:
           logger.exception('Error adding secondary owner.')
   ```

------

1. 要运行该程序，请输入以下命令。

   ```
   python add_secondary_owner.py
   ```

若要查看 `vehicle-registration` 分类账中的这些更改，请参阅[步骤 6：查看文档修订历史记录](getting-started.python.step-6.md)。

# 步骤 6：查看文档修订历史记录
<a name="getting-started.python.step-6"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在上一步中修改车辆注册数据后，您可查询其所有注册车主的历史记录以及任何其他更新的字段。在此步骤中，您将在 `vehicle-registration` 分类账的 `VehicleRegistration` 表格中查询文档的修订历史记录。

**若要查看修订历史记录**

1. 编译并运行 `query_history.py` 程序，以使用 VIN `1N4AL11D75C109151`查询`VehicleRegistration`文档的修订历史记录。

------
#### [ 3.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from datetime import datetime, timedelta
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.model.sample_data import print_result, get_document_ids, SampleData
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.connect_to_ledger import create_qldb_driver
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def format_date_time(date_time):
       """
       Format the given date time to a string.
   
       :type date_time: :py:class:`datetime.datetime`
       :param date_time: The date time to format.
   
       :rtype: str
       :return: The formatted date time.
       """
       return date_time.strftime('`%Y-%m-%dT%H:%M:%S.%fZ`')
   
   
   def previous_primary_owners(driver, vin):
       """
       Find previous primary owners for the given VIN in a single transaction.
       In this example, query the `VehicleRegistration` history table to find all previous primary owners for a VIN.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type vin: str
       :param vin: VIN to find previous primary owners for.
       """
       person_ids = driver.execute_lambda(lambda executor: get_document_ids(executor,
                                                                            Constants.VEHICLE_REGISTRATION_TABLE_NAME,
                                                                            'VIN', vin))
   
       todays_date = datetime.utcnow() - timedelta(seconds=1)
       three_months_ago = todays_date - timedelta(days=90)
       query = 'SELECT data.Owners.PrimaryOwner, metadata.version FROM history({}, {}, {}) AS h WHERE h.metadata.id = ?'.\
           format(Constants.VEHICLE_REGISTRATION_TABLE_NAME, format_date_time(three_months_ago),
                  format_date_time(todays_date))
   
       for ids in person_ids:
           logger.info("Querying the 'VehicleRegistration' table's history using VIN: {}.".format(vin))
           cursor = driver.execute_lambda(lambda executor: executor.execute_statement(query, ids))
           if not (print_result(cursor)) > 0:
               logger.info('No modification history found within the given time frame for document ID: {}'.format(ids))
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Query a table's history for a particular set of documents.
       """
       try:
           with create_qldb_driver(ledger_name) as driver:
               vin = SampleData.VEHICLE_REGISTRATION[0]['VIN']
               previous_primary_owners(driver, vin)
               logger.info('Successfully queried history.')
       except Exception as e:
           logger.exception('Unable to query history to find previous owners.')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```

------
#### [ 2.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from datetime import datetime, timedelta
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.model.sample_data import print_result, get_document_ids, SampleData
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.connect_to_ledger import create_qldb_session
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def format_date_time(date_time):
       """
       Format the given date time to a string.
   
       :type date_time: :py:class:`datetime.datetime`
       :param date_time: The date time to format.
   
       :rtype: str
       :return: The formatted date time.
       """
       return date_time.strftime('`%Y-%m-%dT%H:%M:%S.%fZ`')
   
   
   def previous_primary_owners(transaction_executor, vin):
       """
       Find previous primary owners for the given VIN in a single transaction.
       In this example, query the `VehicleRegistration` history table to find all previous primary owners for a VIN.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type vin: str
       :param vin: VIN to find previous primary owners for.
       """
       person_ids = get_document_ids(transaction_executor, Constants.VEHICLE_REGISTRATION_TABLE_NAME, 'VIN', vin)
   
       todays_date = datetime.utcnow() - timedelta(seconds=1)
       three_months_ago = todays_date - timedelta(days=90)
       query = 'SELECT data.Owners.PrimaryOwner, metadata.version FROM history({}, {}, {}) AS h WHERE h.metadata.id = ?'.\
           format(Constants.VEHICLE_REGISTRATION_TABLE_NAME, format_date_time(three_months_ago),
                  format_date_time(todays_date))
   
       for ids in person_ids:
           logger.info("Querying the 'VehicleRegistration' table's history using VIN: {}.".format(vin))
           cursor = transaction_executor.execute_statement(query, ids)
           if not (print_result(cursor)) > 0:
               logger.info('No modification history found within the given time frame for document ID: {}'.format(ids))
   
   
   if __name__ == '__main__':
       """
       Query a table's history for a particular set of documents.
       """
       try:
           with create_qldb_session() as session:
               vin = SampleData.VEHICLE_REGISTRATION[0]['VIN']
               session.execute_lambda(lambda lambda_executor: previous_primary_owners(lambda_executor, vin),
                                      lambda retry_attempt: logger.info('Retrying due to OCC conflict...'))
               logger.info('Successfully queried history.')
       except Exception:
           logger.exception('Unable to query history to find previous owners.')
   ```

------
**注意**  
您可以通过以下语法查询内置版本[历史记录函数](working.history.md#working.history.function)，以查看文档的修订历史记录。  

     ```
     SELECT * FROM history( table_name [, `start-time` [, `end-time` ] ] ) AS h
     [ WHERE h.metadata.id = 'id' ]
     ```
*开始时间*和*结束时间*均为可选。它们是 Amazon Ion 的字面值，可以用反引号（``...``）表示。要了解更多信息，请参阅 [在 Amazon QLDB 中使用 PartiQL 查询 Ion](ql-reference.query.md)。
最佳做法是，使用日期范围（*开始时间* 和 *结束时间*）和文档 ID（`metadata.id`）来限定历史记录查询。QLDB 处理事务中的`SELECT`查询，这些查询受[事务超时限制](limits.md#limits.fixed)的约束。  
QLDB 历史记录按文档 ID 编制索引，目前无法创建其他历史索引。包含开始时间和结束时间的历史记录查询将从日期范围限定中获得便利。

1. 要运行该程序，请输入以下命令。

   ```
   python query_history.py
   ```

要以加密方式验证 `vehicle-registration` 分类账中的文档修订版，请继续 [第 7 步：验证分类账中的文档](getting-started.python.step-7.md)。

# 第 7 步：验证分类账中的文档
<a name="getting-started.python.step-7"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

借助 Amazon QLDB，您可在 SHA-256 中使用加密哈希，从而有效地验证分类账日记账中文档的完整性。要详细了解验证和加密哈希在 QLDB 中的工作原理，请参阅 [Amazon QLDB 中的数据验证](verification.md)。

在此步骤中，您将验证 `vehicle-registration` 分类账 `VehicleRegistration` 表格中的单据修订版本。首先，您请求一份摘要，该摘要作为输出文件返回，并作为分类账整个变更历史记录的签名。然后，您要求提供与此摘要相关的修订证明。使用此证明，如果所有验证检查都可通过，可以验证修订版的完整性。

**验证文档的修订版**

1. 查看以下 `.py` 文件，这些文件代表了验证所需的 QLDB 对象，以及一个带有用于将 QLDB 响应类型转换为字符串的辅助函数的实用模块。

   1. `block_address.py`

      ```
      # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
      # SPDX-License-Identifier: MIT-0
      #
      # Permission is hereby granted, free of charge, to any person obtaining a copy of this
      # software and associated documentation files (the "Software"), to deal in the Software
      # without restriction, including without limitation the rights to use, copy, modify,
      # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
      # permit persons to whom the Software is furnished to do so.
      #
      # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
      # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
      # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
      # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
      # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
      # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
      
      
      def block_address_to_dictionary(ion_dict):
          """
          Convert a block address from IonPyDict into a dictionary.
          Shape of the dictionary must be: {'IonText': "{strandId: <"strandId">, sequenceNo: <sequenceNo>}"}
      
          :type ion_dict: :py:class:`amazon.ion.simple_types.IonPyDict`/str
          :param ion_dict: The block address value to convert.
      
          :rtype: dict
          :return: The converted dict.
          """
          block_address = {'IonText': {}}
          if not isinstance(ion_dict, str):
              py_dict = '{{strandId: "{}", sequenceNo:{}}}'.format(ion_dict['strandId'], ion_dict['sequenceNo'])
              ion_dict = py_dict
          block_address['IonText'] = ion_dict
          return block_address
      ```

   1. `verifier.py`

      ```
      # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
      # SPDX-License-Identifier: MIT-0
      #
      # Permission is hereby granted, free of charge, to any person obtaining a copy of this
      # software and associated documentation files (the "Software"), to deal in the Software
      # without restriction, including without limitation the rights to use, copy, modify,
      # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
      # permit persons to whom the Software is furnished to do so.
      #
      # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
      # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
      # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
      # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
      # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
      # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
      #
      # This code expects that you have AWS credentials setup per:
      # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
      from array import array
      from base64 import b64encode
      from functools import reduce
      from hashlib import sha256
      from random import randrange
      
      from amazon.ion.simpleion import loads
      
      HASH_LENGTH = 32
      UPPER_BOUND = 8
      
      
      def parse_proof(value_holder):
          """
          Parse the Proof object returned by QLDB into an iterator.
      
          The Proof object returned by QLDB is a dictionary like the following:
          {'IonText': '[{{<hash>}},{{<hash>}}]'}
      
          :type value_holder: dict
          :param value_holder: A structure containing an Ion string value.
      
          :rtype: :py:class:`amazon.ion.simple_types.IonPyList`
          :return: A list of hash values.
          """
          value_holder = value_holder.get('IonText')
          proof_list = loads(value_holder)
          return proof_list
      
      
      def parse_block(value_holder):
          """
          Parse the Block object returned by QLDB and retrieve block hash.
      
          :type value_holder: dict
          :param value_holder: A structure containing an Ion string value.
      
          :rtype: :py:class:`amazon.ion.simple_types.IonPyBytes`
          :return: The block hash.
          """
          value_holder = value_holder.get('IonText')
          block = loads(value_holder)
          block_hash = block.get('blockHash')
          return block_hash
      
      
      def flip_random_bit(original):
          """
          Flip a single random bit in the given hash value.
          This method is used to demonstrate QLDB's verification features.
      
          :type original: bytes
          :param original: The hash value to alter.
      
          :rtype: bytes
          :return: The altered hash with a single random bit changed.
          """
          assert len(original) != 0, 'Invalid bytes.'
      
          altered_position = randrange(len(original))
          bit_shift = randrange(UPPER_BOUND)
          altered_hash = bytearray(original).copy()
      
          altered_hash[altered_position] = altered_hash[altered_position] ^ (1 << bit_shift)
          return bytes(altered_hash)
      
      
      def compare_hash_values(hash1, hash2):
          """
          Compare two hash values by converting them into byte arrays, assuming they are little endian.
      
          :type hash1: bytes
          :param hash1: The hash value to compare.
      
          :type hash2: bytes
          :param hash2: The hash value to compare.
      
          :rtype: int
          :return: Zero if the hash values are equal, otherwise return the difference of the first pair of non-matching bytes.
          """
          assert len(hash1) == HASH_LENGTH
          assert len(hash2) == HASH_LENGTH
      
          hash_array1 = array('b', hash1)
          hash_array2 = array('b', hash2)
      
          for i in range(len(hash_array1) - 1, -1, -1):
              difference = hash_array1[i] - hash_array2[i]
              if difference != 0:
                  return difference
          return 0
      
      
      def join_hash_pairwise(hash1, hash2):
          """
          Take two hash values, sort them, concatenate them, and generate a new hash value from the concatenated values.
      
          :type hash1: bytes
          :param hash1: Hash value to concatenate.
      
          :type hash2: bytes
          :param hash2: Hash value to concatenate.
      
          :rtype: bytes
          :return: The new hash value generated from concatenated hash values.
          """
          if len(hash1) == 0:
              return hash2
          if len(hash2) == 0:
              return hash1
      
          concatenated = hash1 + hash2 if compare_hash_values(hash1, hash2) < 0 else hash2 + hash1
          new_hash_lib = sha256()
          new_hash_lib.update(concatenated)
          new_digest = new_hash_lib.digest()
          return new_digest
      
      
      def calculate_root_hash_from_internal_hashes(internal_hashes, leaf_hash):
          """
          Combine the internal hashes and the leaf hash until only one root hash remains.
      
          :type internal_hashes: map
          :param internal_hashes: An iterable over a list of hash values.
      
          :type leaf_hash: bytes
          :param leaf_hash: The revision hash to pair with the first hash in the Proof hashes list.
      
          :rtype: bytes
          :return: The root hash constructed by combining internal hashes.
          """
          root_hash = reduce(join_hash_pairwise, internal_hashes, leaf_hash)
          return root_hash
      
      
      def build_candidate_digest(proof, leaf_hash):
          """
          Build the candidate digest representing the entire ledger from the Proof hashes.
      
          :type proof: dict
          :param proof: The Proof object.
      
          :type leaf_hash: bytes
          :param leaf_hash: The revision hash to pair with the first hash in the Proof hashes list.
      
          :rtype: bytes
          :return: The calculated root hash.
          """
          parsed_proof = parse_proof(proof)
          root_hash = calculate_root_hash_from_internal_hashes(parsed_proof, leaf_hash)
          return root_hash
      
      
      def verify_document(document_hash, digest, proof):
          """
          Verify document revision against the provided digest.
      
          :type document_hash: bytes
          :param document_hash: The SHA-256 value representing the document revision to be verified.
      
          :type digest: bytes
          :param digest: The SHA-256 hash value representing the ledger digest.
      
          :type proof: dict
          :param proof: The Proof object retrieved from :func:`pyqldbsamples.get_revision.get_revision`.
      
          :rtype: bool
          :return: If the document revision verify against the ledger digest.
          """
          candidate_digest = build_candidate_digest(proof, document_hash)
          return digest == candidate_digest
      
      
      def to_base_64(input):
          """
          Encode input in base64.
      
          :type input: bytes
          :param input: Input to be encoded.
      
          :rtype: string
          :return: Return input that has been encoded in base64.
          """
          encoded_value = b64encode(input)
          return str(encoded_value, 'UTF-8')
      ```

   1. `qldb_string_utils.py`

      ```
      # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
      # SPDX-License-Identifier: MIT-0
      #
      # Permission is hereby granted, free of charge, to any person obtaining a copy of this
      # software and associated documentation files (the "Software"), to deal in the Software
      # without restriction, including without limitation the rights to use, copy, modify,
      # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
      # permit persons to whom the Software is furnished to do so.
      #
      # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
      # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
      # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
      # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
      # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
      # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
      from amazon.ion.simpleion import dumps, loads
      
      
      def value_holder_to_string(value_holder):
          """
          Returns the string representation of a given `value_holder`.
      
          :type value_holder: dict
          :param value_holder: The `value_holder` to convert to string.
      
          :rtype: str
          :return: The string representation of the supplied `value_holder`.
          """
          ret_val = dumps(loads(value_holder), binary=False, indent='  ', omit_version_marker=True)
          val = '{{ IonText: {}}}'.format(ret_val)
          return val
      
      
      def block_response_to_string(block_response):
          """
          Returns the string representation of a given `block_response`.
      
          :type block_response: dict
          :param block_response: The `block_response` to convert to string.
      
          :rtype: str
          :return: The string representation of the supplied `block_response`.
          """
          string = ''
          if block_response.get('Block', {}).get('IonText') is not None:
              string += 'Block: ' + value_holder_to_string(block_response['Block']['IonText']) + ', '
      
          if block_response.get('Proof', {}).get('IonText') is not None:
              string += 'Proof: ' + value_holder_to_string(block_response['Proof']['IonText'])
      
          return '{' + string + '}'
      
      
      def digest_response_to_string(digest_response):
          """
          Returns the string representation of a given `digest_response`.
      
          :type digest_response: dict
          :param digest_response: The `digest_response` to convert to string.
      
          :rtype: str
          :return: The string representation of the supplied `digest_response`.
          """
          string = ''
          if digest_response.get('Digest') is not None:
              string += 'Digest: ' + str(digest_response['Digest']) + ', '
      
          if digest_response.get('DigestTipAddress', {}).get('IonText') is not None:
              string += 'DigestTipAddress: ' + value_holder_to_string(digest_response['DigestTipAddress']['IonText'])
      
          return '{' + string + '}'
      ```

1. 使用两个`.py`文件（`get_digest.py`和`get_revision.py`）执行以下步骤：
   + 从 `vehicle-registration` 分类账中请求新摘要。
   + 通过 `VehicleRegistration` 表的 VIN `1N4AL11D75C109151`，索取每份文件修订版的证明。
   + 通过重新计算摘要，使用返回的摘要来验证修订版。

   `get_digest.py` 文件包含以下代码。

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from boto3 import client
   
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.qldb.qldb_string_utils import digest_response_to_string
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   qldb_client = client('qldb')
   
   
   def get_digest_result(name):
       """
       Get the digest of a ledger's journal.
   
       :type name: str
       :param name: Name of the ledger to operate on.
   
       :rtype: dict
       :return: The digest in a 256-bit hash value and a block address.
       """
       logger.info("Let's get the current digest of the ledger named {}".format(name))
       result = qldb_client.get_digest(Name=name)
       logger.info('Success. LedgerDigest: {}.'.format(digest_response_to_string(result)))
       return result
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       This is an example for retrieving the digest of a particular ledger.
       """
       try:
           get_digest_result(ledger_name)
       except Exception as e:
           logger.exception('Unable to get a ledger digest!')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```
**注意**  
使用该 `get_digest_result` 方法请求一份涵盖分类账中日记账当前*提示*的摘要。日记账的提示指的是 QLDB 收到您的请求时的最近提交的数据块。

   `get_revision.py` 文件包含以下代码。

------
#### [ 3.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from amazon.ion.simpleion import loads
   from boto3 import client
   
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.get_digest import get_digest_result
   from pyqldbsamples.model.sample_data import SampleData, convert_object_to_ion
   from pyqldbsamples.qldb.block_address import block_address_to_dictionary
   from pyqldbsamples.verifier import verify_document, flip_random_bit, to_base_64
   from pyqldbsamples.connect_to_ledger import create_qldb_driver
   from pyqldbsamples.qldb.qldb_string_utils import value_holder_to_string
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   qldb_client = client('qldb')
   
   
   def get_revision(ledger_name, document_id, block_address, digest_tip_address):
       """
       Get the revision data object for a specified document ID and block address.
       Also returns a proof of the specified revision for verification.
   
       :type ledger_name: str
       :param ledger_name: Name of the ledger containing the document to query.
   
       :type document_id: str
       :param document_id: Unique ID for the document to be verified, contained in the committed view of the document.
   
       :type block_address: dict
       :param block_address: The location of the block to request.
   
       :type digest_tip_address: dict
       :param digest_tip_address: The latest block location covered by the digest.
   
       :rtype: dict
       :return: The response of the request.
       """
       result = qldb_client.get_revision(Name=ledger_name, BlockAddress=block_address, DocumentId=document_id,
                                         DigestTipAddress=digest_tip_address)
       return result
   
   
   def lookup_registration_for_vin(driver, vin):
       """
       Query revision history for a particular vehicle for verification.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type vin: str
       :param vin: VIN to query the revision history of a specific registration with.
   
       :rtype: :py:class:`pyqldb.cursor.buffered_cursor.BufferedCursor`
       :return: Cursor on the result set of the statement query.
       """
       logger.info("Querying the 'VehicleRegistration' table for VIN: {}...".format(vin))
       query = 'SELECT * FROM _ql_committed_VehicleRegistration WHERE data.VIN = ?'
       return driver.execute_lambda(lambda txn: txn.execute_statement(query, convert_object_to_ion(vin)))
   
   
   def verify_registration(driver, ledger_name, vin):
       """
       Verify each version of the registration for the given VIN.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type ledger_name: str
       :param ledger_name: The ledger to get digest from.
   
       :type vin: str
       :param vin: VIN to query the revision history of a specific registration with.
   
       :raises AssertionError: When verification failed.
       """
       logger.info("Let's verify the registration with VIN = {}, in ledger = {}.".format(vin, ledger_name))
       digest = get_digest_result(ledger_name)
       digest_bytes = digest.get('Digest')
       digest_tip_address = digest.get('DigestTipAddress')
   
       logger.info('Got a ledger digest: digest tip address = {}, digest = {}.'.format(
           value_holder_to_string(digest_tip_address.get('IonText')), to_base_64(digest_bytes)))
   
       logger.info('Querying the registration with VIN = {} to verify each version of the registration...'.format(vin))
       cursor = lookup_registration_for_vin(driver, vin)
       logger.info('Getting a proof for the document.')
   
       for row in cursor:
           block_address = row.get('blockAddress')
           document_id = row.get('metadata').get('id')
   
           result = get_revision(ledger_name, document_id, block_address_to_dictionary(block_address), digest_tip_address)
           revision = result.get('Revision').get('IonText')
           document_hash = loads(revision).get('hash')
   
           proof = result.get('Proof')
           logger.info('Got back a proof: {}.'.format(proof))
   
           verified = verify_document(document_hash, digest_bytes, proof)
           if not verified:
               raise AssertionError('Document revision is not verified.')
           else:
               logger.info('Success! The document is verified.')
   
           altered_document_hash = flip_random_bit(document_hash)
           logger.info("Flipping one bit in the document's hash and assert that the document is NOT verified. "
                       "The altered document hash is: {}.".format(to_base_64(altered_document_hash)))
           verified = verify_document(altered_document_hash, digest_bytes, proof)
           if verified:
               raise AssertionError('Expected altered document hash to not be verified against digest.')
           else:
               logger.info('Success! As expected flipping a bit in the document hash causes verification to fail.')
   
           logger.info('Finished verifying the registration with VIN = {} in ledger = {}.'.format(vin, ledger_name))
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Verify the integrity of a document revision in a QLDB ledger.
       """
       registration = SampleData.VEHICLE_REGISTRATION[0]
       vin = registration['VIN']
       try:
           with create_qldb_driver(ledger_name) as driver:
               verify_registration(driver, ledger_name, vin)
       except Exception as e:
           logger.exception('Unable to verify revision.')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```

------
#### [ 2.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from amazon.ion.simpleion import loads
   from boto3 import client
   
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.get_digest import get_digest_result
   from pyqldbsamples.model.sample_data import SampleData, convert_object_to_ion
   from pyqldbsamples.qldb.block_address import block_address_to_dictionary
   from pyqldbsamples.verifier import verify_document, flip_random_bit, to_base_64
   from pyqldbsamples.connect_to_ledger import create_qldb_session
   from pyqldbsamples.qldb.qldb_string_utils import value_holder_to_string
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   qldb_client = client('qldb')
   
   
   def get_revision(ledger_name, document_id, block_address, digest_tip_address):
       """
       Get the revision data object for a specified document ID and block address.
       Also returns a proof of the specified revision for verification.
   
       :type ledger_name: str
       :param ledger_name: Name of the ledger containing the document to query.
   
       :type document_id: str
       :param document_id: Unique ID for the document to be verified, contained in the committed view of the document.
   
       :type block_address: dict
       :param block_address: The location of the block to request.
   
       :type digest_tip_address: dict
       :param digest_tip_address: The latest block location covered by the digest.
   
       :rtype: dict
       :return: The response of the request.
       """
       result = qldb_client.get_revision(Name=ledger_name, BlockAddress=block_address, DocumentId=document_id,
                                         DigestTipAddress=digest_tip_address)
       return result
   
   
   def lookup_registration_for_vin(qldb_session, vin):
       """
       Query revision history for a particular vehicle for verification.
   
       :type qldb_session: :py:class:`pyqldb.session.qldb_session.QldbSession`
       :param qldb_session: An instance of the QldbSession class.
   
       :type vin: str
       :param vin: VIN to query the revision history of a specific registration with.
   
       :rtype: :py:class:`pyqldb.cursor.buffered_cursor.BufferedCursor`
       :return: Cursor on the result set of the statement query.
       """
       logger.info("Querying the 'VehicleRegistration' table for VIN: {}...".format(vin))
       query = 'SELECT * FROM _ql_committed_VehicleRegistration WHERE data.VIN = ?'
       parameters = [convert_object_to_ion(vin)]
       cursor = qldb_session.execute_statement(query, parameters)
       return cursor
   
   
   def verify_registration(qldb_session, ledger_name, vin):
       """
       Verify each version of the registration for the given VIN.
   
       :type qldb_session: :py:class:`pyqldb.session.qldb_session.QldbSession`
       :param qldb_session: An instance of the QldbSession class.
   
       :type ledger_name: str
       :param ledger_name: The ledger to get digest from.
   
       :type vin: str
       :param vin: VIN to query the revision history of a specific registration with.
   
       :raises AssertionError: When verification failed.
       """
       logger.info("Let's verify the registration with VIN = {}, in ledger = {}.".format(vin, ledger_name))
       digest = get_digest_result(ledger_name)
       digest_bytes = digest.get('Digest')
       digest_tip_address = digest.get('DigestTipAddress')
   
       logger.info('Got a ledger digest: digest tip address = {}, digest = {}.'.format(
           value_holder_to_string(digest_tip_address.get('IonText')), to_base_64(digest_bytes)))
   
       logger.info('Querying the registration with VIN = {} to verify each version of the registration...'.format(vin))
       cursor = lookup_registration_for_vin(qldb_session, vin)
       logger.info('Getting a proof for the document.')
   
       for row in cursor:
           block_address = row.get('blockAddress')
           document_id = row.get('metadata').get('id')
   
           result = get_revision(ledger_name, document_id, block_address_to_dictionary(block_address), digest_tip_address)
           revision = result.get('Revision').get('IonText')
           document_hash = loads(revision).get('hash')
   
           proof = result.get('Proof')
           logger.info('Got back a proof: {}.'.format(proof))
   
           verified = verify_document(document_hash, digest_bytes, proof)
           if not verified:
               raise AssertionError('Document revision is not verified.')
           else:
               logger.info('Success! The document is verified.')
   
           altered_document_hash = flip_random_bit(document_hash)
           logger.info("Flipping one bit in the document's hash and assert that the document is NOT verified. "
                       "The altered document hash is: {}.".format(to_base_64(altered_document_hash)))
           verified = verify_document(altered_document_hash, digest_bytes, proof)
           if verified:
               raise AssertionError('Expected altered document hash to not be verified against digest.')
           else:
               logger.info('Success! As expected flipping a bit in the document hash causes verification to fail.')
   
           logger.info('Finished verifying the registration with VIN = {} in ledger = {}.'.format(vin, ledger_name))
   
   
   if __name__ == '__main__':
       """
       Verify the integrity of a document revision in a QLDB ledger.
       """
       registration = SampleData.VEHICLE_REGISTRATION[0]
       vin = registration['VIN']
       try:
           with create_qldb_session() as session:
               verify_registration(session, Constants.LEDGER_NAME, vin)
       except Exception:
           logger.exception('Unable to verify revision.')
   ```

------
**注意**  
在`get_revision`方法返回指定文档修订版本的证明后，此程序使用客户端 API 验证该修订版。

1. 要运行该程序，请输入以下命令。

   ```
   python get_revision.py
   ```

如果您不再需要使用`vehicle-registration`分类账，请继续[第 8 步（可选）：清除资源](getting-started.python.step-8.md)。

# 第 8 步（可选）：清除资源
<a name="getting-started.python.step-8"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

您可以继续使用 `vehicle-registration` 分类账。但是，如果您不再需要，请将其删除。

**删除分类账**

1. 使用以下程序（`delete_ledger.py`）以删除您的 `vehicle-registration` 分类账及其所有内容。

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   from time import sleep
   
   from boto3 import client
   
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.describe_ledger import describe_ledger
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   qldb_client = client('qldb')
   
   LEDGER_DELETION_POLL_PERIOD_SEC = 20
   
   
   def delete_ledger(ledger_name):
       """
       Send a request to QLDB to delete the specified ledger.
   
       :type ledger_name: str
       :param ledger_name: Name for the ledger to be deleted.
   
       :rtype: dict
       :return: Result from the request.
       """
       logger.info('Attempting to delete the ledger with name: {}...'.format(ledger_name))
       result = qldb_client.delete_ledger(Name=ledger_name)
       logger.info('Success.')
       return result
   
   
   def wait_for_deleted(ledger_name):
       """
       Wait for the ledger to be deleted.
   
       :type ledger_name: str
       :param ledger_name: The ledger to check on.
       """
       logger.info('Waiting for the ledger to be deleted...')
       while True:
           try:
               describe_ledger(ledger_name)
               logger.info('The ledger is still being deleted. Please wait...')
               sleep(LEDGER_DELETION_POLL_PERIOD_SEC)
           except qldb_client.exceptions.ResourceNotFoundException:
               logger.info('Success. The ledger is deleted.')
               break
   
   
   def set_deletion_protection(ledger_name, deletion_protection):
       """
       Update an existing ledger's deletion protection.
   
       :type ledger_name: str
       :param ledger_name: Name of the ledger to update.
   
       :type deletion_protection: bool
       :param deletion_protection: Enable or disable the deletion protection.
   
       :rtype: dict
       :return: Result from the request.
       """
       logger.info("Let's set deletion protection to {} for the ledger with name {}.".format(deletion_protection,
                                                                                             ledger_name))
       result = qldb_client.update_ledger(Name=ledger_name, DeletionProtection=deletion_protection)
       logger.info('Success. Ledger updated: {}'.format(result))
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Delete a ledger.
       """
       try:
           set_deletion_protection(ledger_name, False)
           delete_ledger(ledger_name)
           wait_for_deleted(ledger_name)
       except Exception as e:
           logger.exception('Unable to delete the ledger.')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```
**注意**  
如果分类账启用了删除保护，您必须先禁用它，然后才能使用 QLDB API 删除分类账。

   `delete_ledger.py` 文件还依赖于以下程序（`describe_ledger.py`）。

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from boto3 import client
   
   from pyqldbsamples.constants import Constants
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   qldb_client = client('qldb')
   
   
   def describe_ledger(ledger_name):
       """
       Describe a ledger.
   
       :type ledger_name: str
       :param ledger_name: Name of the ledger to describe.
       """
       logger.info('describe ledger with name: {}.'.format(ledger_name))
       result = qldb_client.describe_ledger(Name=ledger_name)
       result.pop('ResponseMetadata')
       logger.info('Success. Ledger description: {}.'.format(result))
       return result
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Describe a QLDB ledger.
       """
       try:
           describe_ledger(ledger_name)
       except Exception as e:
           logger.exception('Unable to describe a ledger.')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```

1. 要运行该程序，请输入以下命令。

   ```
   python delete_ledger.py
   ```

# 使用 Amazon QLDB 中的 Amazon Ion 数据类型
<a name="driver-working-with-ion"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

Amazon QLDB 以 Amazon Ion格式存储数据。要在 QLDB 中处理数据，必须使用 [Ion 库](http://amzn.github.io/ion-docs/libs.html)作为支持的编程语言依赖项。

本节学习如何将数据从原生类型转换为离子等效物，反之亦然。本参考指南显示了使用 QLDB 驱动程序处理 QLDB 分类账 Ion 数据代码示例。它包括 Java、.NET (C\$1)、Go、Node.js (TypeScript) 和 Python 的代码示例。

**Topics**
+ [

## 先决条件
](#driver-ion-prereqs)
+ [

## 布尔型
](#driver-ion-bool)
+ [

## Int
](#driver-ion-int)
+ [

## 浮点型
](#driver-ion-float)
+ [

## 十进制
](#driver-ion-decimal)
+ [

## Timestamp
](#driver-ion-timestamp)
+ [

## 字符串
](#driver-ion-string)
+ [

## Blob
](#driver-ion-blob)
+ [

## 列表
](#driver-ion-list)
+ [

## 结构体
](#driver-ion-struct)
+ [

## 空值与动态类型
](#driver-ion-null)
+ [

## 向下转换至 JSON
](#driver-ion-json)

## 先决条件
<a name="driver-ion-prereqs"></a>

以下代码示例假设您有一个 QLDB 驱动程序实例，该实例连接到一个名为`ExampleTable`的表的活动分类账。该表文档中，有一个文档包含以下八个字段：
+ ExampleBool
+ ExampleInt
+ ExampleFloat
+ ExampleDecimal
+ ExampleTimestamp
+ ExampleString
+ ExampleBlob
+ ExampleList

**注意**  
就本参考而言，假定存储在每个字段中的类型与其名称相匹配。实际上，QLDB 并不强制文档字段架构或数据类型定义。

## 布尔型
<a name="driver-ion-bool"></a>

以下代码示例介绍如何处理 Ion 布尔类型。

------
#### [ Java ]

```
// Instantiate an IonSystem from the Ion library
IonSystem ionSystem = IonSystemBuilder.standard().build();

boolean exampleBoolean = driver.execute((txn) -> {
    // Transforming a Java boolean to Ion
    boolean aBoolean = true;
    IonValue ionBool = ionSystem.newBool(aBoolean);

    // Insertion into QLDB
    txn.execute("UPDATE ExampleTable SET ExampleBool = ?", ionBool);

    // Fetching from QLDB
    Result result = txn.execute("SELECT VALUE ExampleBool from ExampleTable");
    // Assume there is only one document in ExampleTable
    for (IonValue ionValue : result)
    {
        // Transforming Ion to a Java boolean
        // Cast IonValue to IonBool first
        aBoolean = ((IonBool)ionValue).booleanValue();
    }

    // exampleBoolean is now the value fetched from QLDB
    return aBoolean;
});
```

------
#### [ .NET ]

```
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;
...

IValueFactory valueFactory = new ValueFactory();

// Transforming a C# bool to Ion.
bool nativeBool  = true;
IIonValue ionBool = valueFactory.NewBool(nativeBool);

IAsyncResult selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute("UPDATE ExampleTable SET ExampleBool = ?", ionBool);

    // Fetching from QLDB.
    return await txn.Execute("SELECT VALUE ExampleBool from ExampleTable");
});

bool? retrievedBool = null;

// Assume there is only one document in ExampleTable.
await foreach (IIonValue ionValue in selectResult)
{
    // Transforming Ion to a C# bool.
    retrievedBool = ionValue.BoolValue;
}
```

**注意**  
要转换为同步代码，请移除 `await` 和 `async` 关键字，然后将 `IAsyncResult` 类型更改为 `IResult`。

------
#### [ Go ]

```
exampleBool, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
	aBool := true

	// Insertion into QLDB
	_, err = txn.Execute("UPDATE ExampleTable SET ExampleBool = ?", aBool)
	if err != nil {
		return nil, err
	}

	// Fetching from QLDB
	result, err := txn.Execute("SELECT VALUE ExampleBool FROM ExampleTable")
	if err != nil {
		return nil, err
	}

	// Assume there is only one document in ExampleTable
	if result.Next(txn) {
		var decodedResult bool
		err := ion.Unmarshal(result.GetCurrentData(), &decodedResult)
		if err != nil {
			return nil, err
		}

		// exampleBool is now the value fetched from QLDB
		return decodedResult, nil
	}
	return nil, result.Err()
})
```

------
#### [ Node.js ]

```
async function queryIonBoolean(driver: QldbDriver): Promise<boolean> {
    return (driver.executeLambda(async (txn: TransactionExecutor) => {
            // Updating QLDB
            await txn.execute("UPDATE ExampleTable SET ExampleBool = ?", true);

            // Fetching from QLDB
            const resultList: dom.Value[] = (await txn.execute("SELECT VALUE ExampleBool FROM ExampleTable")).getResultList();

            // Assume there is only one document in ExampleTable
            const ionValue: dom.Value = resultList[0];
            // Transforming Ion to a TypeScript Boolean
            const boolValue: boolean = ionValue.booleanValue();
            return boolValue;
        })
    );
}
```

------
#### [ Python ]

```
def update_and_query_ion_bool(txn):
    # QLDB can take in a Python bool
    a_bool = True

    # Insertion into QLDB
    txn.execute_statement("UPDATE ExampleTable SET ExampleBool = ?", a_bool)

    # Fetching from QLDB
    cursor = txn.execute_statement("SELECT VALUE ExampleBool FROM ExampleTable")

    # Assume there is only one document in ExampleTable
    for ion_value in cursor:
        # Ion Python bool is a child class of Python bool
        a_bool = ion_value

    # example_bool is now the value fetched from QLDB
    return a_bool


example_bool = driver.execute_lambda(lambda txn: update_and_query_ion_bool(txn))
```

------

## Int
<a name="driver-ion-int"></a>

以下代码示例介绍如何处理 Ion 整数类型。

------
#### [ Java ]

```
// Instantiate an IonSystem from the Ion library
IonSystem ionSystem = IonSystemBuilder.standard().build();

int exampleInt = driver.execute((txn) -> {
    // Transforming a Java int to Ion
    int aInt = 256;
    IonValue ionInt = ionSystem.newInt(aInt);

    // Insertion into QLDB
    txn.execute("UPDATE ExampleTable SET ExampleInt = ?", ionInt);

    // Fetching from QLDB
    Result result = txn.execute("SELECT VALUE ExampleInt from ExampleTable");
    // Assume there is only one document in ExampleTable
    for (IonValue ionValue : result)
    {
        // Transforming Ion to a Java int
        // Cast IonValue to IonInt first
        aInt = ((IonInt)ionValue).intValue();
    }

    // exampleInt is now the value fetched from QLDB
    return aInt;
});
```

------
#### [ .NET ]

```
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;
...

IValueFactory valueFactory = new ValueFactory();

// Transforming a C# int to Ion.
int nativeInt  = 256;
IIonValue ionInt = valueFactory.NewInt(nativeInt);

IAsyncResult selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute("UPDATE ExampleTable SET ExampleInt = ?", ionInt);

    // Fetching from QLDB.
    return await txn.Execute("SELECT VALUE ExampleInt from ExampleTable");
});

int? retrievedInt = null;

// Assume there is only one document in ExampleTable.
await foreach (IIonValue ionValue in selectResult)
{
    // Transforming Ion to a C# int.
    retrievedInt = ionValue.IntValue;
}
```

**注意**  
要转换为同步代码，请移除 `await` 和 `async` 关键字，然后将 `IAsyncResult` 类型更改为 `IResult`。

------
#### [ Go ]

```
exampleInt, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
	aInt := 256

	// Insertion into QLDB
	_, err = txn.Execute("UPDATE ExampleTable SET ExampleInt = ?", aInt)
	if err != nil {
		return nil, err
	}

	// Fetching from QLDB
	result, err := txn.Execute("SELECT VALUE ExampleInt FROM ExampleTable")
	if err != nil {
		return nil, err
	}

	// Assume there is only one document in ExampleTable
	if result.Next(txn) {
		var decodedResult int
		err := ion.Unmarshal(result.GetCurrentData(), &decodedResult)
		if err != nil {
			return nil, err
		}

		// exampleInt is now the value fetched from QLDB
		return decodedResult, nil
	}
	return nil, result.Err()
})
```

------
#### [ Node.js ]

```
async function queryIonInt(driver: QldbDriver): Promise<number> {
    return (driver.executeLambda(async (txn: TransactionExecutor) => {
            // Updating QLDB
            await txn.execute("UPDATE ExampleTable SET ExampleInt = ?", 256);

            // Fetching from QLDB
            const resultList: dom.Value[] = (await txn.execute("SELECT VALUE ExampleInt FROM ExampleTable")).getResultList();

            // Assume there is only one document in ExampleTable
            const ionValue: dom.Value = resultList[0];
            // Transforming Ion to a TypeScript Number
            const intValue: number = ionValue.numberValue();
            return intValue;
        })
    );
}
```

------
#### [ Python ]

```
def update_and_query_ion_int(txn):
    # QLDB can take in a Python int
    a_int = 256

    # Insertion into QLDB
    txn.execute_statement("UPDATE ExampleTable SET ExampleInt = ?", a_int)

    # Fetching from QLDB
    cursor = txn.execute_statement("SELECT VALUE ExampleInt FROM ExampleTable")

    # Assume there is only one document in ExampleTable
    for ion_value in cursor:
        # Ion Python int is a child class of Python int
        a_int = ion_value

    # example_int is now the value fetched from QLDB
    return a_int


example_int = driver.execute_lambda(lambda txn: update_and_query_ion_int(txn))
```

------

## 浮点型
<a name="driver-ion-float"></a>

以下代码示例介绍如何处理 Ion 浮动类型。

------
#### [ Java ]

```
// Instantiate an IonSystem from the Ion library
IonSystem ionSystem = IonSystemBuilder.standard().build();

float exampleFloat = driver.execute((txn) -> {
    // Transforming a Java float to Ion
    float aFloat = 256;
    IonValue ionFloat = ionSystem.newFloat(aFloat);

    // Insertion into QLDB
    txn.execute("UPDATE ExampleTable SET ExampleFloat = ?", ionFloat);

    // Fetching from QLDB
    Result result = txn.execute("SELECT VALUE ExampleFloat from ExampleTable");
    // Assume there is only one document in ExampleTable
    for (IonValue ionValue : result)
    {
        // Transforming Ion to a Java float
        // Cast IonValue to IonFloat first
        aFloat = ((IonFloat)ionValue).floatValue();
    }

    // exampleFloat is now the value fetched from QLDB
    return aFloat;
});
```

------
#### [ .NET ]

**使用 C\$1 浮点数**

```
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;
...

IValueFactory valueFactory = new ValueFactory();

// Transforming a C# float to Ion.
float nativeFloat = 256;
IIonValue ionFloat = valueFactory.NewFloat(nativeFloat);

IAsyncResult selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute("UPDATE ExampleTable SET ExampleFloat = ?", ionFloat);

    // Fetching from QLDB.
    return await txn.Execute("SELECT VALUE ExampleFloat from ExampleTable");
});

float? retrievedFloat = null;

// Assume there is only one document in ExampleTable.
await foreach (IIonValue ionValue in selectResult)
{
    // Transforming Ion to a C# float. We cast ionValue.DoubleValue to a float
    // but be cautious, this is a down-cast and can lose precision.
    retrievedFloat = (float)ionValue.DoubleValue;
}
```

**注意**  
要转换为同步代码，请移除 `await` 和 `async` 关键字，然后将 `IAsyncResult` 类型更改为 `IResult`。

**使用 C\$1 双**

```
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;
...

IValueFactory valueFactory = new ValueFactory();

// Transforming a C# double to Ion.
double nativeDouble = 256;
IIonValue ionFloat = valueFactory.NewFloat(nativeDouble);

IAsyncResult selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute("UPDATE ExampleTable SET ExampleFloat = ?", ionFloat);

    // Fetching from QLDB.
    return await txn.Execute("SELECT VALUE ExampleFloat from ExampleTable");
});

double? retrievedDouble = null;

// Assume there is only one document in ExampleTable.
await foreach (IIonValue ionValue in selectResult)
{
    // Transforming Ion to a C# double.
    retrievedDouble = ionValue.DoubleValue;
}
```

**注意**  
要转换为同步代码，请移除 `await` 和 `async` 关键字，然后将 `IAsyncResult` 类型更改为 `IResult`。

------
#### [ Go ]

```
exampleFloat, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
	aFloat := float32(256)

	// Insertion into QLDB
	_, err = txn.Execute("UPDATE ExampleTable SET ExampleFloat = ?", aFloat)
	if err != nil {
		return nil, err
	}

	// Fetching from QLDB
	result, err := txn.Execute("SELECT VALUE ExampleFloat FROM ExampleTable")
	if err != nil {
		return nil, err
	}

	// Assume there is only one document in ExampleTable
	if result.Next(txn) {
		// float64 would work as well
		var decodedResult float32
		err := ion.Unmarshal(result.GetCurrentData(), &decodedResult)
		if err != nil {
			return nil, err
		}

		// exampleFloat is now the value fetched from QLDB
		return decodedResult, nil
	}
	return nil, result.Err()
})
```

------
#### [ Node.js ]

```
async function queryIonFloat(driver: QldbDriver): Promise<number> {
    return (driver.executeLambda(async (txn: TransactionExecutor) => {
            // Updating QLDB
            await txn.execute("UPDATE ExampleTable SET ExampleFloat = ?", 25.6);

            // Fetching from QLDB
            const resultList: dom.Value[] = (await txn.execute("SELECT VALUE ExampleFloat FROM ExampleTable")).getResultList();

            // Assume there is only one document in ExampleTable
            const ionValue: dom.Value = resultList[0];
            // Transforming Ion to a TypeScript Number
            const floatValue: number = ionValue.numberValue();
            return floatValue;
        })
    );
}
```

------
#### [ Python ]

```
def update_and_query_ion_float(txn):
    # QLDB can take in a Python float
    a_float = float(256)

    # Insertion into QLDB
    txn.execute_statement("UPDATE ExampleTable SET ExampleFloat = ?", a_float)

    # Fetching from QLDB
    cursor = txn.execute_statement("SELECT VALUE ExampleFloat FROM ExampleTable")

    # Assume there is only one document in ExampleTable
    for ion_value in cursor:
        # Ion Python float is a child class of Python float
        a_float = ion_value

    # example_float is now the value fetched from QLDB
    return a_float


example_float = driver.execute_lambda(lambda txn: update_and_query_ion_float(txn))
```

------

## 十进制
<a name="driver-ion-decimal"></a>

以下代码示例介绍如何处理 Ion 小数类型。

------
#### [ Java ]

```
// Instantiate an IonSystem from the Ion library
IonSystem ionSystem = IonSystemBuilder.standard().build();

double exampleDouble = driver.execute((txn) -> {
    // Transforming a Java double to Ion
    double aDouble = 256;
    IonValue ionDecimal = ionSystem.newDecimal(aDouble);

    // Insertion into QLDB
    txn.execute("UPDATE ExampleTable SET ExampleDecimal = ?", ionDecimal);

    // Fetching from QLDB
    Result result = txn.execute("SELECT VALUE ExampleDecimal from ExampleTable");
    // Assume there is only one document in ExampleTable
    for (IonValue ionValue : result)
    {
        // Transforming Ion to a Java double
        // Cast IonValue to IonDecimal first
        aDouble = ((IonDecimal)ionValue).doubleValue();
    }

    // exampleDouble is now the value fetched from QLDB
    return aDouble;
});
```

------
#### [ .NET ]

```
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;
...

IValueFactory valueFactory = new ValueFactory();

// Transforming a C# decimal to Ion.
decimal nativeDecimal = 256.8723m;
IIonValue ionDecimal = valueFactory.NewDecimal(nativeDecimal);

IAsyncResult selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute("UPDATE ExampleTable SET ExampleDecimal = ?", ionDecimal);

    // Fetching from QLDB.
    return await txn.Execute("SELECT VALUE ExampleDecimal from ExampleTable");
});

decimal? retrievedDecimal = null;

// Assume there is only one document in ExampleTable.
await foreach (IIonValue ionValue in selectResult)
{
    // Transforming Ion to a C# decimal.
    retrievedDecimal = ionValue.DecimalValue;
}
```

**注意**  
要转换为同步代码，请移除 `await` 和 `async` 关键字，然后将 `IAsyncResult` 类型更改为 `IResult`。

------
#### [ Go ]

```
exampleDecimal, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
	aDecimal, err := ion.ParseDecimal("256")
	if err != nil {
		return nil, err
	}

	// Insertion into QLDB
	_, err = txn.Execute("UPDATE ExampleTable SET ExampleDecimal = ?", aDecimal)
	if err != nil {
		return nil, err
	}

	// Fetching from QLDB
	result, err := txn.Execute("SELECT VALUE ExampleDecimal FROM ExampleTable")
	if err != nil {
		return nil, err
	}

	// Assume there is only one document in ExampleTable
	if result.Next(txn) {
		var decodedResult ion.Decimal
		err := ion.Unmarshal(result.GetCurrentData(), &decodedResult)
		if err != nil {
			return nil, err
		}

		// exampleDecimal is now the value fetched from QLDB
		return decodedResult, nil
	}
	return nil, result.Err()
})
```

------
#### [ Node.js ]

```
async function queryIonDecimal(driver: QldbDriver): Promise<Decimal> {
    return (driver.executeLambda(async (txn: TransactionExecutor) => {
            // Creating a Decimal value. Decimal is an Ion Type with high precision
            let ionDecimal: Decimal = dom.load("2.5d-6").decimalValue();
            // Updating QLDB
            await txn.execute("UPDATE ExampleTable SET ExampleDecimal = ?", ionDecimal);

            // Fetching from QLDB
            const resultList: dom.Value[] = (await txn.execute("SELECT VALUE ExampleDecimal FROM ExampleTable")).getResultList();

            // Assume there is only one document in ExampleTable
            const ionValue: dom.Value = resultList[0];
            // Get the Ion Decimal
            ionDecimal = ionValue.decimalValue();
            return ionDecimal;
        })
    );
}
```

**注意**  
也可以使用`ionValue.numberValue()`将 Ion 十进制数转换为 JavaScript 数字。使用 `ionValue.numberValue()` 具有更好的性能，但精度较低。

------
#### [ Python ]

```
def update_and_query_ion_decimal(txn):
    # QLDB can take in a Python decimal
    a_decimal = Decimal(256)

    # Insertion into QLDB
    txn.execute_statement("UPDATE ExampleTable SET ExampleDecimal = ?", a_decimal)

    # Fetching from QLDB
    cursor = txn.execute_statement("SELECT VALUE ExampleDecimal FROM ExampleTable")

    # Assume there is only one document in ExampleTable
    for ion_value in cursor:
        # Ion Python decimal is a child class of Python decimal
        a_decimal = ion_value

    # example_decimal is now the value fetched from QLDB
    return a_decimal


example_decimal = driver.execute_lambda(lambda txn: update_and_query_ion_decimal(txn))
```

------

## Timestamp
<a name="driver-ion-timestamp"></a>

以下代码示例介绍如何处理 Ion 时间戳类型。

------
#### [ Java ]

```
// Instantiate an IonSystem from the Ion library
IonSystem ionSystem = IonSystemBuilder.standard().build();

Date exampleDate = driver.execute((txn) -> {
    // Transforming a Java Date to Ion
    Date aDate = new Date();
    IonValue ionTimestamp = ionSystem.newUtcTimestamp(aDate);

    // Insertion into QLDB
    txn.execute("UPDATE ExampleTable SET ExampleTimestamp = ?", ionTimestamp);

    // Fetching from QLDB
    Result result = txn.execute("SELECT VALUE ExampleTimestamp from ExampleTable");
    // Assume there is only one document in ExampleTable
    for (IonValue ionValue : result)
    {
        // Transforming Ion to a Java Date
        // Cast IonValue to IonTimestamp first
        aDate = ((IonTimestamp)ionValue).dateValue();
    }

    // exampleDate is now the value fetched from QLDB
    return aDate;
});
```

------
#### [ .NET ]

```
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;
using Amazon.IonDotnet;
...

IValueFactory valueFactory = new ValueFactory();

// Convert C# native DateTime to Ion.
DateTime nativeDateTime = DateTime.Now;

// First convert it to a timestamp object from Ion.
Timestamp timestamp = new Timestamp(nativeDateTime);
// Then convert to Ion timestamp.
IIonValue ionTimestamp = valueFactory.NewTimestamp(timestamp);

IAsyncResult selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute("UPDATE ExampleTable SET ExampleTimestamp = ?", ionTimestamp);

    // Fetching from QLDB.
    return await txn.Execute("SELECT VALUE ExampleTimestamp from ExampleTable");
});

DateTime? retrievedDateTime = null;

// Assume there is only one document in ExampleTable.
await foreach (IIonValue ionValue in selectResult)
{
    // Transforming Ion to a C# DateTime.
    retrievedDateTime = ionValue.TimestampValue.DateTimeValue;
}
```

**注意**  
要转换为同步代码，请移除 `await` 和 `async` 关键字，然后将 `IAsyncResult` 类型更改为 `IResult`。

------
#### [ Go ]

```
exampleTimestamp, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
	aTimestamp := time.Date(2006, time.May, 20, 12, 30, 0, 0, time.UTC)
	if err != nil {
		return nil, err
	}

	// Insertion into QLDB
	_, err = txn.Execute("UPDATE ExampleTable SET ExampleTimestamp = ?", aTimestamp)
	if err != nil {
		return nil, err
	}

	// Fetching from QLDB
	result, err := txn.Execute("SELECT VALUE ExampleTimestamp FROM ExampleTable")
	if err != nil {
		return nil, err
	}

	// Assume there is only one document in ExampleTable
	if result.Next(txn) {
		var decodedResult ion.Timestamp
		err := ion.Unmarshal(result.GetCurrentData(), &decodedResult)
		if err != nil {
			return nil, err
		}

		// exampleTimestamp is now the value fetched from QLDB
		return decodedResult.GetDateTime(), nil
	}
	return nil, result.Err()
})
```

------
#### [ Node.js ]

```
async function queryIonTimestamp(driver: QldbDriver): Promise<Date> {
    return (driver.executeLambda(async (txn: TransactionExecutor) => {
            let exampleDateTime: Date = new Date(Date.now());
            // Updating QLDB
            await txn.execute("UPDATE ExampleTable SET ExampleTimestamp = ?", exampleDateTime);

            // Fetching from QLDB
            const resultList: dom.Value[] = (await txn.execute("SELECT VALUE ExampleTimestamp FROM ExampleTable")).getResultList();

            // Assume there is only one document in ExampleTable
            const ionValue: dom.Value = resultList[0];
            // Transforming Ion to a TypeScript Date
            exampleDateTime = ionValue.timestampValue().getDate();
            return exampleDateTime;
        })
    );
}
```

------
#### [ Python ]

```
def update_and_query_ion_timestamp(txn):
    # QLDB can take in a Python timestamp
    a_datetime = datetime.now()

    # Insertion into QLDB
    txn.execute_statement("UPDATE ExampleTable SET ExampleTimestamp = ?", a_datetime)

    # Fetching from QLDB
    cursor = txn.execute_statement("SELECT VALUE ExampleTimestamp FROM ExampleTable")

    # Assume there is only one document in ExampleTable
    for ion_value in cursor:
        # Ion Python timestamp is a child class of Python datetime
        a_timestamp = ion_value

    # example_timestamp is now the value fetched from QLDB
    return a_timestamp


example_timestamp = driver.execute_lambda(lambda txn: update_and_query_ion_timestamp(txn))
```

------

## 字符串
<a name="driver-ion-string"></a>

以下代码示例介绍如何处理 Ion 字符串类型。

------
#### [ Java ]

```
// Instantiate an IonSystem from the Ion library
IonSystem ionSystem = IonSystemBuilder.standard().build();

String exampleString = driver.execute((txn) -> {
    // Transforming a Java String to Ion
    String aString = "Hello world!";
    IonValue ionString = ionSystem.newString(aString);

    // Insertion into QLDB
    txn.execute("UPDATE ExampleTable SET ExampleString = ?", ionString);

    // Fetching from QLDB
    Result result = txn.execute("SELECT VALUE ExampleString from ExampleTable");
    // Assume there is only one document in ExampleTable
    for (IonValue ionValue : result)
    {
        // Transforming Ion to a Java String
        // Cast IonValue to IonString first
        aString = ((IonString)ionValue).stringValue();
    }

    // exampleString is now the value fetched from QLDB
    return aString;
});
```

------
#### [ .NET ]

```
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;
...

IValueFactory valueFactory = new ValueFactory();

// Convert C# string to Ion.
String nativeString = "Hello world!";
IIonValue ionString = valueFactory.NewString(nativeString);

IAsyncResult selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute("UPDATE ExampleTable SET ExampleString = ?", ionString);

    // Fetching from QLDB.
    return await txn.Execute("SELECT VALUE ExampleString from ExampleTable");
});

String retrievedString = null;

// Assume there is only one document in ExampleTable.
await foreach (IIonValue ionValue in selectResult)
{
    // Transforming Ion to a C# string.
    retrievedString = ionValue.StringValue;
}
```

**注意**  
要转换为同步代码，请移除 `await` 和 `async` 关键字，然后将 `IAsyncResult` 类型更改为 `IResult`。

------
#### [ Go ]

```
exampleString, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
	aString := "Hello World!"

	// Insertion into QLDB
	_, err = txn.Execute("UPDATE ExampleTable SET ExampleString = ?", aString)
	if err != nil {
		return nil, err
	}

	// Fetching from QLDB
	result, err := txn.Execute("SELECT VALUE ExampleString FROM ExampleTable")
	if err != nil {
		return nil, err
	}

	// Assume there is only one document in ExampleTable
	if result.Next(txn) {
		var decodedResult string
		err := ion.Unmarshal(result.GetCurrentData(), &decodedResult)
		if err != nil {
			return nil, err
		}

		// exampleString is now the value fetched from QLDB
		return decodedResult, nil
	}
	return nil, result.Err()
})
```

------
#### [ Node.js ]

```
async function queryIonString(driver: QldbDriver): Promise<string> {
    return (driver.executeLambda(async (txn: TransactionExecutor) => {
            // Updating QLDB
            await txn.execute("UPDATE ExampleTable SET ExampleString = ?", "Hello World!");

            // Fetching from QLDB
            const resultList: dom.Value[] = (await txn.execute("SELECT VALUE ExampleString FROM ExampleTable")).getResultList();

            // Assume there is only one document in ExampleTable
            const ionValue: dom.Value = resultList[0];
            // Transforming Ion to a TypeScript String
            const stringValue: string = ionValue.stringValue();
            return stringValue;
        })
    );
}
```

------
#### [ Python ]

```
def update_and_query_ion_string(txn):
    # QLDB can take in a Python string
    a_string = "Hello world!"

    # Insertion into QLDB
    txn.execute_statement("UPDATE ExampleTable SET ExampleString = ?", a_string)

    # Fetching from QLDB
    cursor = txn.execute_statement("SELECT VALUE ExampleString FROM ExampleTable")

    # Assume there is only one document in ExampleTable
    for ion_value in cursor:
        # Ion Python string is a child class of Python string
        a_string = ion_value

    # example_string is now the value fetched from QLDB
    return a_string


example_string = driver.execute_lambda(lambda txn: update_and_query_ion_string(txn))
```

------

## Blob
<a name="driver-ion-blob"></a>

以下代码示例介绍如何处理 Ion 二进制大对象类型。

------
#### [ Java ]

```
// Instantiate an IonSystem from the Ion library
IonSystem ionSystem = IonSystemBuilder.standard().build();

byte[] exampleBytes = driver.execute((txn) -> {
    // Transforming a Java byte array to Ion
    // Transform any arbitrary data to a byte array to store in QLDB
    String aString = "Hello world!";
    byte[] aByteArray = aString.getBytes();
    IonValue ionBlob = ionSystem.newBlob(aByteArray);

    // Insertion into QLDB
    txn.execute("UPDATE ExampleTable SET ExampleBlob = ?", ionBlob);

    // Fetching from QLDB
    Result result = txn.execute("SELECT VALUE ExampleBlob from ExampleTable");
    // Assume there is only one document in ExampleTable
    for (IonValue ionValue : result)
    {
        // Transforming Ion to a Java byte array
        // Cast IonValue to IonBlob first
        aByteArray = ((IonBlob)ionValue).getBytes();
    }

    // exampleBytes is now the value fetched from QLDB
    return aByteArray;
});
```

------
#### [ .NET ]

```
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;
using System.Text;
...

IValueFactory valueFactory = new ValueFactory();

// Transform any arbitrary data to a byte array to store in QLDB.
string nativeString = "Hello world!";
byte[] nativeByteArray = Encoding.UTF8.GetBytes(nativeString);
// Transforming a C# byte array to Ion.
IIonValue ionBlob = valueFactory.NewBlob(nativeByteArray);

IAsyncResult selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute("UPDATE ExampleTable SET ExampleBlob = ?", ionBlob);

    // Fetching from QLDB.
    return await txn.Execute("SELECT VALUE ExampleBlob from ExampleTable");
});

byte[] retrievedByteArray = null;

// Assume there is only one document in ExampleTable.
await foreach (IIonValue ionValue in selectResult)
{
    // Transforming Ion to a C# byte array.
    retrievedByteArray = ionValue.Bytes().ToArray();
}
```

**注意**  
要转换为同步代码，请移除 `await` 和 `async` 关键字，然后将 `IAsyncResult` 类型更改为 `IResult`。

------
#### [ Go ]

```
exampleBlob, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
	aBlob := []byte("Hello World!")

	// Insertion into QLDB
	_, err = txn.Execute("UPDATE ExampleTable SET ExampleBlob = ?", aBlob)
	if err != nil {
		return nil, err
	}

	// Fetching from QLDB
	result, err := txn.Execute("SELECT VALUE ExampleBlob FROM ExampleTable")
	if err != nil {
		return nil, err
	}

	// Assume there is only one document in ExampleTable
	if result.Next(txn) {
		var decodedResult []byte
		err := ion.Unmarshal(result.GetCurrentData(), &decodedResult)
		if err != nil {
			return nil, err
		}

		// exampleBlob is now the value fetched from QLDB
		return decodedResult, nil
	}
	return nil, result.Err()
})
```

------
#### [ Node.js ]

```
async function queryIonBlob(driver: QldbDriver): Promise<Uint8Array> {
    return (driver.executeLambda(async (txn: TransactionExecutor) => {
            const enc = new TextEncoder();
            let blobValue: Uint8Array = enc.encode("Hello World!");
            // Updating QLDB
            await txn.execute("UPDATE ExampleTable SET ExampleBlob = ?", blobValue);

            // Fetching from QLDB
            const resultList: dom.Value[] = (await txn.execute("SELECT VALUE ExampleBlob FROM ExampleTable")).getResultList();

            // Assume there is only one document in ExampleTable
            const ionValue: dom.Value = resultList[0];
            // Transforming Ion to TypeScript Uint8Array
            blobValue = ionValue.uInt8ArrayValue();
            return blobValue;
        })
    );
}
```

------
#### [ Python ]

```
def update_and_query_ion_blob(txn):
    # QLDB can take in a Python byte array
    a_string = "Hello world!"
    a_byte_array = str.encode(a_string)

    # Insertion into QLDB
    txn.execute_statement("UPDATE ExampleTable SET ExampleBlob = ?", a_byte_array)

    # Fetching from QLDB
    cursor = txn.execute_statement("SELECT VALUE ExampleBlob FROM ExampleTable")

    # Assume there is only one document in ExampleTable
    for ion_value in cursor:
        # Ion Python blob is a child class of Python byte array
        a_blob = ion_value

    # example_blob is now the value fetched from QLDB
    return a_blob


example_blob = driver.execute_lambda(lambda txn: update_and_query_ion_blob(txn))
```

------

## 列表
<a name="driver-ion-list"></a>

以下代码示例介绍如何处理 Ion 列表类型。

------
#### [ Java ]

```
// Instantiate an IonSystem from the Ion library
IonSystem ionSystem = IonSystemBuilder.standard().build();

List<Integer> exampleList = driver.execute((txn) -> {
    // Transforming a Java List to Ion
    List<Integer> aList = new ArrayList<>();
    // Add 5 Integers to the List for the sake of example
    for (int i = 0; i < 5; i++) {
        aList.add(i);
    }
    // Create an empty Ion List
    IonList ionList = ionSystem.newEmptyList();
    // Add the 5 Integers to the Ion List
    for (Integer i : aList) {
        // Convert each Integer to Ion ints first to add it to the Ion List
        ionList.add(ionSystem.newInt(i));
    }

    // Insertion into QLDB
    txn.execute("UPDATE ExampleTable SET ExampleList = ?", (IonValue) ionList);

    // Fetching from QLDB
    Result result = txn.execute("SELECT VALUE ExampleList from ExampleTable");
    // Assume there is only one document in ExampleTable
    for (IonValue ionValue : result)
    {
        // Iterate through the Ion List to map it to a Java List
        List<Integer> intList = new ArrayList<>();
        for (IonValue ionInt : (IonList)ionValue) {
            // Convert the 5 Ion ints to Java Integers
            intList.add(((IonInt)ionInt).intValue());
        }
        aList = intList;
    }

    // exampleList is now the value fetched from QLDB
    return aList;
});
```

------
#### [ .NET ]

```
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;
using System.Collections.Generic;
...

IValueFactory valueFactory = new ValueFactory();

// Transforming a C# list to Ion.
IIonValue ionList = valueFactory.NewEmptyList();
foreach (int i in new List<int> {0, 1, 2, 3, 4})
{
    // Convert to Ion int and add to Ion list.
    ionList.Add(valueFactory.NewInt(i));
}

IAsyncResult selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute("UPDATE ExampleTable SET ExampleList = ?", ionList);

    // Fetching from QLDB.
    return await txn.Execute("SELECT VALUE ExampleList from ExampleTable");
});


List<int> retrievedList = new List<int>();
await foreach (IIonValue ionValue in selectResult)
{
    // Iterate through the Ion List to map it to a C# list.
    foreach (IIonValue ionInt in ionValue)
    {
        retrievedList.Add(ionInt.IntValue);
    }
}
```

**注意**  
要转换为同步代码，请移除 `await` 和 `async` 关键字，然后将 `IAsyncResult` 类型更改为 `IResult`。

------
#### [ Go ]

```
exampleList, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
	aList := []int{1, 2, 3, 4, 5}

	// Insertion into QLDB
	_, err = txn.Execute("UPDATE ExampleTable SET ExampleList = ?", aList)
	if err != nil {
		return nil, err
	}

	// Fetching from QLDB
	result, err := txn.Execute("SELECT VALUE ExampleList FROM ExampleTable")
	if err != nil {
		return nil, err
	}

	// Assume there is only one document in ExampleTable
	if result.Next(txn) {
		var decodedResult []int
		err := ion.Unmarshal(result.GetCurrentData(), &decodedResult)
		if err != nil {
			return nil, err
		}

		// exampleList is now the value fetched from QLDB
		return decodedResult, nil
	}
	return nil, result.Err()
})
```

------
#### [ Node.js ]

```
async function queryIonList(driver: QldbDriver): Promise<number[]> {
    return (driver.executeLambda(async (txn: TransactionExecutor) => {
            let listOfNumbers: number[] = [1, 2, 3, 4, 5];
            // Updating QLDB
            await txn.execute("UPDATE ExampleTable SET ExampleList = ?", listOfNumbers);

            // Fetching from QLDB
            const resultList: dom.Value[] = (await txn.execute("SELECT VALUE ExampleList FROM ExampleTable")).getResultList();

            // Assume there is only one document in ExampleTable
            const ionValue: dom.Value = resultList[0];
            // Get Ion List
            const ionList: dom.Value[] = ionValue.elements();
            // Iterate through the Ion List to map it to a JavaScript Array
            let intList: number[] = [];
            ionList.forEach(item => {
                // Transforming Ion to a TypeScript Number
                const intValue: number = item.numberValue();
                intList.push(intValue);
            });
            listOfNumbers = intList;
            return listOfNumbers;
        })
    );
}
```

------
#### [ Python ]

```
def update_and_query_ion_list(txn):
    # QLDB can take in a Python list
    a_list = list()
    for i in range(0, 5):
        a_list.append(i)

    # Insertion into QLDB
    txn.execute_statement("UPDATE ExampleTable SET ExampleList = ?", a_list)

    # Fetching from QLDB
    cursor = txn.execute_statement("SELECT VALUE ExampleList FROM ExampleTable")

    # Assume there is only one document in ExampleTable
    for ion_value in cursor:
        # Ion Python blob is a child class of Python list
        a_list = ion_value

    # example_list is now the value fetched from QLDB
    return a_list


example_list = driver.execute_lambda(lambda txn: update_and_query_ion_list(txn))
```

------

## 结构体
<a name="driver-ion-struct"></a>

在 QLDB 中，`struct`数据类型与其他 Ion 类型特别不同。插入到表格中的顶级文档必须属于 `struct`类型。文档字段也可以存储嵌套 `struct`。

为简单起见，以下示例定义了一个仅包含 `ExampleString` 和 `ExampleInt` 字段的文档。

------
#### [ Java ]

```
class ExampleStruct {
    public String exampleString;
    public int exampleInt;

    public ExampleStruct(String exampleString, int exampleInt) {
        this.exampleString = exampleString;
        this.exampleInt = exampleInt;
    }
}

// Instantiate an IonSystem from the Ion library
IonSystem ionSystem = IonSystemBuilder.standard().build();

ExampleStruct examplePojo = driver.execute((txn) -> {
    // Transforming a POJO to Ion
    ExampleStruct aPojo = new ExampleStruct("Hello world!", 256);
    // Create an empty Ion struct
    IonStruct ionStruct = ionSystem.newEmptyStruct();
    // Map the fields of the POJO to Ion values and put them in the Ion struct
    ionStruct.add("ExampleString", ionSystem.newString(aPojo.exampleString));
    ionStruct.add("ExampleInt", ionSystem.newInt(aPojo.exampleInt));

    // Insertion into QLDB
    txn.execute("INSERT INTO ExampleTable ?", ionStruct);

    // Fetching from QLDB
    Result result = txn.execute("SELECT * from ExampleTable");
    // Assume there is only one document in ExampleTable
    for (IonValue ionValue : result)
    {
        // Map the fields of the Ion struct to Java values and construct a new POJO
        ionStruct = (IonStruct)ionValue;
        IonString exampleString = (IonString)ionStruct.get("ExampleString");
        IonInt exampleInt = (IonInt)ionStruct.get("ExampleInt");
        aPojo = new ExampleStruct(exampleString.stringValue(), exampleInt.intValue());
    }

    // examplePojo is now the document fetched from QLDB
    return aPojo;
});
```

或者，您可以使用 [Jackson 库](https://github.com/FasterXML/jackson-dataformats-binary/tree/master/ion) 将数据类型从 Ion 映射出入。该库支持其他 Ion 数据类型，但此示例重点介绍 `struct` 类型。

```
class ExampleStruct {
    public String exampleString;
    public int exampleInt;

    @JsonCreator
    public ExampleStruct(@JsonProperty("ExampleString") String exampleString,
                         @JsonProperty("ExampleInt") int exampleInt) {
        this.exampleString = exampleString;
        this.exampleInt = exampleInt;
    }

    @JsonProperty("ExampleString")
    public String getExampleString() {
        return this.exampleString;
    }

    @JsonProperty("ExampleInt")
    public int getExampleInt() {
        return this.exampleInt;
    }
}

// Instantiate an IonSystem from the Ion library
IonSystem ionSystem = IonSystemBuilder.standard().build();
// Instantiate an IonObjectMapper from the Jackson library
IonObjectMapper ionMapper = new IonValueMapper(ionSystem);

ExampleStruct examplePojo = driver.execute((txn) -> {
    // Transforming a POJO to Ion
    ExampleStruct aPojo = new ExampleStruct("Hello world!", 256);
    IonValue ionStruct;
    try  {
        // Use the mapper to convert Java objects into Ion
        ionStruct = ionMapper.writeValueAsIonValue(aPojo);
    } catch (IOException e) {
        // Wrap the exception and throw it for the sake of simplicity in this example
        throw new RuntimeException(e);
    }

    // Insertion into QLDB
    txn.execute("INSERT INTO ExampleTable ?", ionStruct);

    // Fetching from QLDB
    Result result = txn.execute("SELECT * from ExampleTable");
    // Assume there is only one document in ExampleTable
    for (IonValue ionValue : result)
    {
        // Use the mapper to convert Ion to Java objects
        try {
            aPojo = ionMapper.readValue(ionValue, ExampleStruct.class);
        } catch (IOException e) {
            // Wrap the exception and throw it for the sake of simplicity in this example
            throw new RuntimeException(e);
        }
    }

    // examplePojo is now the document fetched from QLDB
    return aPojo;
});
```

------
#### [ .NET ]

使用 [Amazon.QLDB.Driver.Serialization](https://www.nuget.org/packages/Amazon.QLDB.Driver.Serialization/) 库将原生 C\$1 数据类型从 Ion 映射出入。该库支持其他 Ion 数据类型，但此示例重点介绍 `struct` 类型。

```
using Amazon.QLDB.Driver.Generic;
using Amazon.QLDB.Driver.Serialization;
...

IAsyncQldbDriver driver = AsyncQldbDriver.Builder()
    .WithLedger("vehicle-registration")
    // Add Serialization library
    .WithSerializer(new ObjectSerializer())
    .Build();

// Creating a C# POCO.
ExampleStruct exampleStruct = new ExampleStruct
{
    ExampleString = "Hello world!",
    ExampleInt = 256
};

IAsyncResult<ExampleStruct> selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute(txn.Query<Document>("UPDATE ExampleTable SET ExampleStruct = ?", exampleStruct));

    // Fetching from QLDB.
    return await txn.Execute(txn.Query<ExampleStruct>("SELECT VALUE ExampleStruct from ExampleTable"));
});

await foreach (ExampleStruct row in selectResult)
{
    Console.WriteLine(row.ExampleString);
    Console.WriteLine(row.ExampleInt);
}
```

**注意**  
要转换为同步代码，请移除 `await` 和 `async` 关键字，然后将 `IAsyncResult` 类型更改为 `IResult`。

或者，你可以使用 [Amazon。 IonDotnet.Builders](https://www.nuget.org/packages/Amazon.IonDotnet/) 库用于处理 Ion 数据类型。

```
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;
...

IValueFactory valueFactory = new ValueFactory();

// Creating Ion struct.
IIonValue ionStruct = valueFactory.NewEmptyStruct();
ionStruct.SetField("ExampleString", valueFactory.NewString("Hello world!"));
ionStruct.SetField("ExampleInt", valueFactory.NewInt(256));

IAsyncResult selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute("UPDATE ExampleTable SET ExampleStruct = ?", ionStruct);

    // Fetching from QLDB.
    return await txn.Execute("SELECT VALUE ExampleStruct from ExampleTable");
});

string retrievedString = null;
int? retrievedInt = null;

await foreach (IIonValue ionValue in selectResult)
{
    retrievedString = ionValue.GetField("ExampleString").StringValue;
    retrievedInt = ionValue.GetField("ExampleInt").IntValue;
}
```

------
#### [ Go ]

```
exampleStruct, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
	aStruct := map[string]interface{} {
		"ExampleString": "Hello World!",
		"ExampleInt": 256,
	}

	// Insertion into QLDB
	_, err = txn.Execute("UPDATE ExampleTable SET ExampleStruct = ?", aStruct)
	if err != nil {
		return nil, err
	}

	// Fetching from QLDB
	result, err := txn.Execute("SELECT VALUE ExampleStruct FROM ExampleTable")
	if err != nil {
		return nil, err
	}

	// Assume there is only one document in ExampleTable
	if result.Next(txn) {
		var decodedResult map[string]interface{}
		err := ion.Unmarshal(result.GetCurrentData(), &decodedResult)
		if err != nil {
			return nil, err
		}

		// exampleStruct is now the value fetched from QLDB
		return decodedResult, nil
	}
	return nil, result.Err()
})
```

------
#### [ Node.js ]

```
async function queryIonStruct(driver: QldbDriver): Promise<any> {
    let exampleStruct: any = {stringValue: "Hello World!", intValue: 256};
    return (driver.executeLambda(async (txn: TransactionExecutor) => {
            // Inserting into QLDB
            await txn.execute("INSERT INTO ExampleTable ?", exampleStruct);

            // Fetching from QLDB
            const resultList: dom.Value[] = (await txn.execute("SELECT * FROM ExampleTable")).getResultList();

            // Assume there is only one document in ExampleTable
            const ionValue: dom.Value = resultList[0];
            // We can get all the keys of Ion struct and their associated values
            const ionFieldNames: string[] = ionValue.fieldNames();

            // Getting key and value of Ion struct to TypeScript String and Number
            const nativeStringVal: string = ionValue.get(ionFieldNames[0]).stringValue();
            const nativeIntVal: number = ionValue.get(ionFieldNames[1]).numberValue();
            // Alternatively, we can access to Ion struct fields, using their literal field names:
            //   const nativeStringVal = ionValue.get("stringValue").stringValue();
            //   const nativeIntVal = ionValue.get("intValue").numberValue();

            exampleStruct = {[ionFieldNames[0]]: nativeStringVal, [ionFieldNames[1]]: nativeIntVal};
            return exampleStruct;
        })
    );
}
```

------
#### [ Python ]

```
def update_and_query_ion_struct(txn):
    # QLDB can take in a Python struct
    a_struct = {"ExampleString": "Hello world!", "ExampleInt": 256}

    # Insertion into QLDB
    txn.execute_statement("UPDATE ExampleTable SET ExampleStruct = ?", a_struct)

    # Fetching from QLDB
    cursor = txn.execute_statement("SELECT VALUE ExampleStruct FROM ExampleTable")

    # Assume there is only one document in ExampleTable
    for ion_value in cursor:
        # Ion Python struct is a child class of Python struct
        a_struct = ion_value

    # example_struct is now the value fetched from QLDB
    return a_struct


example_struct = driver.execute_lambda(lambda txn: update_and_query_ion_struct(txn))
```

------

## 空值与动态类型
<a name="driver-ion-null"></a>

QLDB 支持开放内容，且不强制文档字段定义架构或数据类型定义。您也可在 QLDB 文档中存储 Ion 空值。前述示例都假设返回的每种数据类型都是已知的，并且不是 null。以下示例说明如何在 Ion 数据类型未知或可能 null 时与其结合使用。

------
#### [ Java ]

```
// Empty variables
String exampleString = null;
Integer exampleInt = null;

// Assume ionValue is some queried data from QLDB
IonValue ionValue = null;

// Check the value type and assign it to the variable if it is not null
if (ionValue.getType() == IonType.STRING) {
    if (ionValue.isNullValue()) {
        exampleString = null;
    } else {
        exampleString = ((IonString)ionValue).stringValue();
    }
} else if (ionValue.getType() == IonType.INT) {
    if (ionValue.isNullValue()) {
        exampleInt = null;
    } else {
        exampleInt = ((IonInt)ionValue).intValue();
    }
};

// Creating null values
IonSystem ionSystem = IonSystemBuilder.standard().build();

// A null value still has an Ion type
IonString ionString;
if (exampleString == null) {
    // Specifically a null string
    ionString = ionSystem.newNullString();
} else {
    ionString = ionSystem.newString(exampleString);
}

IonInt ionInt;
if (exampleInt == null) {
    // Specifically a null int
    ionInt = ionSystem.newNullInt();
} else {
    ionInt = ionSystem.newInt(exampleInt);
}

// Special case regarding null!
// There is a generic null type which has Ion type 'Null'.
// The above null values still have the types 'String' and 'Int'
IonValue specialNull = ionSystem.newNull();
if (specialNull.getType() == IonType.NULL) {
    // This is true!
}
if (specialNull.isNullValue()) {
    // This is also true!
}
if (specialNull.getType() == IonType.STRING || specialNull.getType() == IonType.INT) {
    // This is false!
}
```

------
#### [ .NET ]

```
// Empty variables.
string exampleString = null;
int? exampleInt = null;

// Assume ionValue is some queried data from QLDB.
IIonValue ionValue;

if (ionValue.Type() == IonType.String)
{
    exampleString = ionValue.StringValue;
}
else if (ionValue.Type() == IonType.Int)
{
    if (ionValue.IsNull)
    {
        exampleInt = null;
    }
    else
    {
        exampleInt = ionValue.IntValue;
    }
};

// Creating null values.
IValueFactory valueFactory = new ValueFactory();

// A null value still has an Ion type.
IIonValue ionString = valueFactory.NewString(exampleString);

IIonValue ionInt;
if (exampleInt == null)
{
    // Specifically a null int.
    ionInt = valueFactory.NewNullInt();
}
else
{
    ionInt = valueFactory.NewInt(exampleInt.Value);
}

// Special case regarding null!
// There is a generic null type which has Ion type 'Null'.
IIonValue specialNull = valueFactory.NewNull();
if (specialNull.Type() == IonType.Null) {
    // This is true!
}
if (specialNull.IsNull) {
    // This is also true!
}
```

------
#### [ Go ]

**编组限制**

在 Go 中，当你编组然后解组它们时，`nil` 值不会保留它们的类型。一个 `nil` 值编组为 Ion null，但是 Ion null 则解组为零值，而不是`nil`。

```
ionNull, err := ion.MarshalText(nil) // ionNull is set to ion null
if err != nil {
    return
}

var result int
err = ion.Unmarshal(ionNull, &result) // result unmarshals to 0
if err != nil {
    return
}
```

------
#### [ Node.js ]

```
// Empty variables
let exampleString: string;
let exampleInt: number;

// Assume ionValue is some queried data from QLDB
// Check the value type and assign it to the variable if it is not null
if (ionValue.getType() === IonTypes.STRING) {
    if (ionValue.isNull()) {
        exampleString = null;
    } else {
        exampleString = ionValue.stringValue();
    }
} else if (ionValue.getType() === IonTypes.INT) {
    if (ionValue.isNull()) {
        exampleInt = null;
    } else {
        exampleInt = ionValue.numberValue();
    }
}

// Creating null values
if (exampleString === null) {
    ionString = dom.load('null.string');
} else {
    ionString = dom.load.of(exampleString);
}

if (exampleInt === null) {
    ionInt = dom.load('null.int');
} else {
    ionInt = dom.load.of(exampleInt);
}

// Special case regarding null!
// There is a generic null type which has Ion type 'Null'.
// The above null values still have the types 'String' and 'Int'
specialNull: dom.Value = dom.load("null.null");
if (specialNull.getType() === IonType.NULL) {
    // This is true!
}
if (specialNull.getType() === IonType.STRING || specialNull.getType() === IonType.INT) {
    // This is false!
}
```

------
#### [ Python ]

```
# Empty variables
example_string = None
example_int = None

# Assume ion_value is some queried data from QLDB
# Check the value type and assign it to the variable if it is not null
if ion_value.ion_type == IonType.STRING:
    if isinstance(ion_value, IonPyNull):
        example_string = None
    else:
        example_string = ion_value
elif ion_value.ion_type == IonType.INT:
    if isinstance(ion_value, IonPyNull):
        example_int = None
    else:
        example_int = ion_value

# Creating Ion null values
if example_string is None:
    # Specifically a null string
    ion_string = loads("null.string")
else:
    # QLDB can take in Python string
    ion_string = example_string
if example_int is None:
    # Specifically a null int
    ion_int = loads("null.int")
else:
    # QLDB can take in Python int
    ion_int = example_int


# Special case regarding null!
# There is a generic null type which has Ion type 'Null'.
# The above null values still have the types 'String' and 'Int'
special_null = loads("null.null")
if special_null.ion_type == IonType.NULL:
    # This is true!
if special_null.ion_type == IonType.STRING or special_null.ion_type == IonType.INT:
    # This is false!
```

------

## 向下转换至 JSON
<a name="driver-ion-json"></a>

如果您的应用程序需要 JSON 兼容性，则可以将 Amazon Ion 数据向下转换至 JSON。但是，在某些情况下，如果您的数据使用 JSON 中不存在的丰富 Ion 类型，则将 Ion 转换为 JSON 时会有损失。

有关 Ion 与 JSON 转换规则的详细信息，请参阅*Amazon Ion Cookbook*中的[向下转换至 JSON](https://amzn.github.io/ion-docs/guides/cookbook.html#down-converting-to-json)。