

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

# 使用扩展自定义 DynamoDB 增强型客户端操作
<a name="ddb-en-client-extensions"></a>

DynamoDB 增强型客户端 API 支持插件扩展，这些插件扩展提供的功能不仅限于映射操作。扩展使用两种钩子方法在读取和写入操作期间修改数据：
+ `beforeWrite()` - 在写入操作实际执行之前对其进行修改
+ `afterRead()` - 在读取操作完成之后对返回的结果进行修改

某些操作（例如项目更新）同时执行写入和读取，因此会调用两种钩子方法。

## 如何加载扩展
<a name="ddb-en-client-extensions-loading"></a>

按照您在增强型客户端生成器中指定的顺序加载扩展。加载顺序可能很重要，因为一个扩展可以对前一个扩展变换过的值起作用。

默认情况下，增强版客户端会加载两个扩展：
+ `[VersionedRecordExtension](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtension.html)` - 提供乐观锁
+ `[AtomicCounterExtension](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/extensions/AtomicCounterExtension.html)` - 自动增加计数器属性

您可以使用增强型客户端生成器覆盖默认行为并加载任何扩展。如果您不想使用默认扩展，也可以指定一个都不用。

**重要**  
如果您加载自己的扩展，则增强型客户端不会加载任何默认扩展。如果您想要任一默认扩展提供的行为，则需要将其明确添加到扩展列表中。

以下示例说明如何在 `VersionedRecordExtension` 之后加载名为 `verifyChecksumExtension` 的自定义扩展。本示例中未加载 `AtomicCounterExtension`。

```
DynamoDbEnhancedClientExtension versionedRecordExtension = VersionedRecordExtension.builder().build();

DynamoDbEnhancedClient enhancedClient = 
    DynamoDbEnhancedClient.builder()
                          .dynamoDbClient(dynamoDbClient)
                          .extensions(versionedRecordExtension, verifyChecksumExtension)
                          .build();
```

## 可用的扩展详细信息和配置
<a name="ddb-en-client-extensions-details"></a>

以下部分提供有关 SDK 中每个可用扩展的详细信息。

### 使用 `VersionedRecordExtension` 实施乐观锁
<a name="ddb-en-client-extensions-VRE"></a>

`VersionedRecordExtension` 扩展提供乐观锁，当项目写入数据库时，它会递增和跟踪项目的版本号。如果实际永久保存的项目的版本号与应用程序上次读取的值不匹配，则会在每次写入时添加一个导致写入失败的条件。

#### 配置
<a name="ddb-en-client-extensions-VRE-conf"></a>

要指定使用哪个属性来跟踪项目版本号，请在表架构中标记数字属性。

以下代码段指定 `version` 属性应包含项目版本号。

```
    @DynamoDbVersionAttribute
    public Integer getVersion() {...};
    public void setVersion(Integer version) {...};
```

下面的代码段显示了等效的静态表架构方法。

```
    .addAttribute(Integer.class, a -> a.name("version")
                                       .getter(Customer::getVersion)
                                       .setter(Customer::setVersion)
                                        // Apply the 'version' tag to the attribute.
                                       .tags(VersionedRecordExtension.AttributeTags.versionAttribute())
```

#### 工作原理
<a name="ddb-en-client-extensions-VRE-how-it-works"></a>

使用 `VersionedRecordExtension` 实现的乐观锁机制会对这些 `DynamoDbEnhancedClient` 和 `DynamoDbTable` 方法产生以下影响：

**`putItem`**  
为新项目分配的初始版本值为 0。可以使用 `@DynamoDbVersionAttribute(startAt = X)` 进行配置。

**`updateItem`**  
如果您检索项目，然后更新它的一个或多个属性，并尝试保存所做更改，那么只有在客户端和服务器端的版本号匹配时操作才能成功。  
如果成功，版本号自动加 1。可以使用 `@DynamoDbVersionAttribute(incrementBy = X)` 进行配置。

**`deleteItem`**  
`DynamoDbVersionAttribute` 注释没有效果。删除项目时，必须手动添加条件表达式。  
以下示例添加了一个条件表达式，以确保删除的项目是读取的项目。在以下示例中，`recordVersion` 是 bean 使用 `@DynamoDbVersionAttribute` 进行注释的属性。  

```
// 1. Read the item and get its current version.
Customer item = customerTable.getItem(Key.builder().partitionValue("someId").build());
// `recordVersion` is the bean's attribute that is annotated with `@DynamoDbVersionAttribute`.
AttributeValue currentVersion = item.getRecordVersion();

// 2. Create conditional delete with the `currentVersion` value.
DeleteItemEnhancedRequest deleteItemRequest =
    DeleteItemEnhancedRequest.builder()
       .key(KEY)
       .conditionExpression(Expression.builder()
           .expression("recordVersion = :current_version_value")
           .putExpressionValue(":current_version_value", currentVersion)
           .build()).build();

customerTable.deleteItem(deleteItemRequest);
```

**`transactWriteItems`**  
+ `addPutItem`：此方法的行为与 `putItem` 相同。
+ `addUpdateItem`：此方法的行为与 `updateItem` 相同。
+ `addDeleteItem`：此方法的行为与 `deleteItem` 相同。

**`batchWriteItem`**  
+ `addPutItem`：此方法的行为与 `putItem` 相同。
+ `addDeleteItem`：此方法的行为与 `deleteItem` 相同。

**注意**  
DynamoDB 全局表在处理并发更新时，使用[‘最后一个写入方为准’协调](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/V2globaltables_HowItWorks.html#V2globaltables_HowItWorks.consistency-modes)策略，DynamoDB 会尽最大努力判断谁是最后一个写入方。如果您使用全局表，这个“最后一个写入方为准”策略意味着锁定策略可能无法按预期工作，因为所有副本最终会基于 DynamoDB 确定的最后一次写入结果达成一致。

#### 如何禁用
<a name="ddb-en-client-extensions-VRE-how-to-disable"></a>

要禁用乐观锁，请不要使用 `@DynamoDbVersionAttribute` 注释。

### 使用 `AtomicCounterExtension` 实施计数器
<a name="ddb-en-client-extensions-ACE"></a>

每次向数据库写入记录时，`AtomicCounterExtension` 扩展都会增加一个带标签的数字属性。您可以指定起始值和增量值。如果未指定任何值，则将起始值设置为 0，属性的值以 1 为增量。

#### 配置
<a name="ddb-en-client-extensions-ACE-conf"></a>

要指定哪个属性是计数器，请在表架构中标记一个类型为 `Long` 的属性。

以下代码段显示了为 `counter` 属性使用默认起始值和增量值的情况。

```
    @DynamoDbAtomicCounter
    public Long getCounter() {...};
    public void setCounter(Long counter) {...};
```

下面的代码段显示了静态表架构方法。原子计数器扩展使用起始值 10，并在每次写入记录时将该值递增 5。

```
    .addAttribute(Integer.class, a -> a.name("counter")
                                       .getter(Customer::getCounter)
                                       .setter(Customer::setCounter)
                                        // Apply the 'atomicCounter' tag to the attribute with start and increment values.
                                       .tags(StaticAttributeTags.atomicCounter(10L, 5L))
```

### 使用 `AutoGeneratedTimestampRecordExtension` 添加时间戳
<a name="ddb-en-client-extensions-AGTE"></a>

每次成功将项目写入数据库时，`AutoGeneratedTimestampRecordExtension` 扩展都会自动使用当前时间戳更新类型为 `[Instant](https://docs.oracle.com/javase/8/docs/api/java/time/Instant.html)` 的已标记属性。默认情况下不加载此扩展。

#### 配置
<a name="ddb-en-client-extensions-AGTE-conf"></a>

要指定将使用当前时间戳更新的属性，请在表架构中标记 `Instant` 属性。

在以下代码段中，`lastUpdate` 属性是扩展行为的目标。请注意该属性必须是 `Instant` 类型的要求。

```
    @DynamoDbAutoGeneratedTimestampAttribute
    public Instant getLastUpdate() {...}
    public void setLastUpdate(Instant lastUpdate) {...}
```

下面的代码段显示了等效的静态表架构方法。

```
     .addAttribute(Instant.class, a -> a.name("lastUpdate")
                                        .getter(Customer::getLastUpdate)
                                        .setter(Customer::setLastUpdate)
                                        // Applying the 'autoGeneratedTimestamp' tag to the attribute.
                                        .tags(AutoGeneratedTimestampRecordExtension.AttributeTags.autoGeneratedTimestampAttribute())
```

### 使用生成一个 UUID AutoGeneratedUuidExtension
<a name="ddb-en-client-extensions-AGUE"></a>

向数据库写入新记录时，`AutoGeneratedUuidExtension` 扩展为属性生成唯一的 UUID（通用唯一标识符）。使用 Java JDK [UUID.randomUUID()](https://docs.oracle.com/javase/8/docs/api/java/util/UUID.html#randomUUID--) 方法并应用于 `java.lang.String` 类型的属性。默认情况下不加载此扩展。

#### 配置
<a name="ddb-en-client-extensions-AGUE-conf"></a>

在以下代码段中，`uniqueId` 属性是扩展行为的目标。

```
    @AutoGeneratedUuidExtension
    public String getUniqueId() {...}
    public void setUniqueId(String uniqueId) {...}
```

下面的代码段显示了等效的静态表架构方法。

```
     .addAttribute(String.class, a -> a.name("uniqueId")
                                        .getter(Customer::getUniqueId)
                                        .setter(Customer::setUniqueId)
                                        // Applying the 'autoGeneratedUuid' tag to the attribute.
                                        .tags(AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute())
```

如果您希望扩展仅为 `putItem` 方法填充 UUID，而不为 `updateItem` 方法填充 UUID，请添加[更新行为](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/mapper/UpdateBehavior.html)注释，如以下代码段所示。

```
    @AutoGeneratedUuidExtension
    @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)
    public String getUniqueId() {...}
    public void setUniqueId(String uniqueId) {...}
```

如果您使用静态表架构方法，请使用以下等效代码。

```
     .addAttribute(String.class, a -> a.name("uniqueId")
                                        .getter(Customer::getUniqueId)
                                        .setter(Customer::setUniqueId)
                                        // Applying the 'autoGeneratedUuid' tag to the attribute.
                                        .tags(AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute(),
                                              StaticAttributeTags.updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))
```

# 自定义扩展示例
<a name="ddb-en-client-extensions-custom"></a>

您可以通过实施 `DynamoDbEnhancedClientExtension` 接口来创建自定义扩展。以下自定义扩展类展示了 `beforeWrite()` 方法，如果数据库中的项目还没有 `registrationDate` 属性，则使用更新表达式来设置该属性。

```
public final class CustomExtension implements DynamoDbEnhancedClientExtension {

    // 1. In a custom extension, use an UpdateExpression to define what action to take before
    //    an item is updated.
    @Override
    public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite context) {
        if ( context.operationContext().tableName().equals("Customer")
                && context.operationName().equals(OperationName.UPDATE_ITEM)) {
            return WriteModification.builder()
                    .updateExpression(createUpdateExpression())
                    .build();
        }
        return WriteModification.builder().build();  // Return an "empty" WriteModification instance if the extension should not be applied.
                                                     // In this case, if the code is not updating an item on the Customer table.
    }

    private static UpdateExpression createUpdateExpression() {

        // 2. Use a SetAction, a subclass of UpdateAction, to provide the values in the update.
        SetAction setAction =
                SetAction.builder()
                        .path("registrationDate")
                        .value("if_not_exists(registrationDate, :regValue)")
                        .putExpressionValue(":regValue", AttributeValue.fromS(Instant.now().toString()))
                        .build();
        // 3. Build the UpdateExpression with one or more UpdateAction.
        return UpdateExpression.builder()
                .addAction(setAction)
                .build();
    }
}
```