SDKで操作しながらDynamoDBの基本を学ぶ


自分用にスニペットを残しつつ、
SDKの各種コマンドとプロパティを網羅しながら理解を深めました。

実務で使ってみたい。

:::note warn
**ここでは試さないこと**
* TransactGet/TransactWrite
* TTL設定
* DynamoDBStream連携
* PartiQL
:::



# CreateTable

```ts
const command = new CreateTableCommand({
  TableName: TABLE_NAME,
  KeySchema: [
    { AttributeName: 'CustomerId', KeyType: 'HASH' }, // partition key
    { AttributeName: 'OrderNumber', KeyType: 'RANGE' }, // sort key
  ],
  AttributeDefinitions: [
    { AttributeName: 'CustomerId', AttributeType: 'S' }, // 属性：String
    { AttributeName: 'OrderNumber', AttributeType: 'N' }, // 属性：Number
  ],
  LocalSecondaryIndexes: [
    {
      IndexName: 'LSI_1',
      KeySchema: [
        { AttributeName: 'CustomerId', KeyType: 'HASH' },
        { AttributeName: 'Date', KeyType: 'RANGE' },
      ],
      Projection: {
        ProjectionType: 'INCLUDE', // || "ALL" || "KEYS_ONLY"
        NonKeyAttributes: ['Price'],
      },
    },
  ],
  GlobalSecondaryIndexes: [
    {
      IndexName: 'GSI_1',
      KeySchema: [
        { AttributeName: 'Product', KeyType: 'HASH' },
        { AttributeName: 'OrderNumber', KeyType: 'RANGE' },
      ],
      Projection: { ProjectionType: 'ALL' }, // || "KEYS_ONLY" || "INCLUDE"
      ProvisionedThroughput: {
        ReadCapacityUnits: 1,
        WriteCapacityUnits: 1,
      },
    },
  ],
  BillingMode: 'PAY_PER_REQUEST', // || "PROVISIONED"
  ProvisionedThroughput: {
    ReadCapacityUnits: 1,
    WriteCapacityUnits: 1,
  },
  StreamSpecification: {
    StreamEnabled: true, // || false
    StreamViewType: 'NEW_IMAGE', //  || "OLD_IMAGE" || "NEW_AND_OLD_IMAGES" || "KEYS_ONLY"
  },
  SSESpecification: {
    Enabled: true, // || false
    SSEType: 'KMS',
    KMSMasterKeyId: 'keyId',
  },
  TableClass: 'STANDARD', //  || "STANDARD_INFREQUENT_ACCESS"
  DeletionProtectionEnabled: true, //  || false
  Tags: [{ Key: 'id', Value: '1' }],
});
```

#### 基本

* 作成後はテーブル名変更不可
* テーブル名以外の変更は`UpdateTableCommand`を使う

#### TableName（必須）

#### KeySchema（必須）
* 「主キー(PartitionKey)」または「複合主キー(PartitionKey + SortKey)」 を指定
* `HASHキー` = PartitionKey
* `RANGEキー` = SortKey

#### AttributeDefinitions（必須）
* PartitionKey および SortKey の型を指定
* [ `S`(String) | `N`(Number) | `B`(binary) ] のいずれかを指定可能

#### LocalSecondaryIndexes

* テーブル作成時のみ追加可能
* SortKeyありのテーブルに対してのみ追加可能
* テーブルにつき最大5個まで作成可能
* PartitionKeyにはメインテーブルと同様の項目指定が必須
* ProjectionType
    * `KEYS_ONLY`：複合主キーの情報のみをINDEXに投影する
    * `INCLUDE`：「複合主キー + `NonKeyAttributes`に指定した属性」をINDEXに投影する
    * `ALL`：すべての属性をINDEXに投影する
* パーティションごとに 10 GB のサイズ制限がある
    * [DynamoDBにおけるパーティションの概念](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/HowItWorks.Partitions.html)
* WCU/RCU はメインテーブルから消費される

:::note
LSIの「ローカル」が意味すること（参考：[コンセプトから学ぶAmazon DynamoDB【LSI篇】](https://dev.classmethod.jp/articles/conceptual-learning-about-dynamodb-lsi/)）

**同じパーティション内に別の規則で整理INDEXを構築する**ため「ローカル」という理解になる模様。
関連して「PartitionKeyはメインテーブルと同じ」「10GBのサイズ制限」などの制約が存在する。
:::

#### GlobalSecondaryIndexes

* テーブル作成時以外でも追加可能
    * 追加時は`CreateGlobalTableCommand`を使う
* GSIはメインテーブルとは異なる PartitionKey を指定して追加可能
* 1テーブルにつき最大20個まで作成可能
* ProjectionType の仕様はLSIと同様
* GSIは **結果整合性** のみをサポート（= **強力な結果整合性** をサポートしないため ConsistentRead の指定は不可）
* ProvisionedThroughput
    * テーブルが**プロビジョンドモード**の場合、独立したスループット設定が必要
    * テーブルが**オンデマンドモード**の場合、設定してもWCU/RCUの値は0になる模様（=設定省略できる）

#### BillingMode

* `PROVISIONED`：プロビジョニングモード
* `PAY_PER_REQUEST`：オンデマンドモード
    * メリット：WCU/RCU の設定値検討や管理がなくなる
    * デメリット：(2018年時点では)キャパシティユニットの使用率約15%を境界としてオンデマンドモードの方が割高になる
    * 参考：[DynamoDBのオンデマンドとプロビジョニングの料金を比較をしてみた #reinvent](https://dev.classmethod.jp/articles/reinvent2018-compare-dynamodb-on-demand-price-with-provisioned-price/)

#### ProvisionedThroughput

* プロビジョニングモードにおいて RCU(ReadCapacityUnits) と WCU(WriteCapacityUnits) の値を設定する
* RCUとWCUの仕様

    |  | 1RCU<br>結果整合性 | 1RCU<br>強い結果整合性 | 1WCU |
    |:-:|:-:|:-:|:-:|
    | read最大サイズ | 4KB | 4KB | - |
    | read実行/秒 | 2 | 1 | - |
    | write最大サイズ | -  | - | 1KB |
    | write実行/秒 | - | - | 1 |




#### StreamSpecification

* DynamoDB Stream の有効化設定
* DynamoDB Stream は DynamoDB に対する項目の**追加・変更・削除**を検出する
* StreamViewType で Stream への書き込み内容を設定できる
    * `KEYS_ONLY`：変更されたアイテムのキー属性のみ
    * `NEW_IMAGE`：変更後に表示されるアイテム全体
    * `OLD_IMAGE`：変更前に表示されていたアイテム全体
    * `NEW_AND_OLD_IMAGES`：アイテムの新しいアイテムと古いアイテムの両方
* 「追加のみをStreamに流す」などの制御はできないが、例えば Lambda ではイベントソースマッピングでフィルタリングができる模様
    * 参考：[【アップデート】Lambdaのイベントソースマッピングでイベントのフィルタが可能になりました](https://dev.classmethod.jp/articles/event-source-mapping-support-filter-expression/)

#### SSESpecification

* ストレージ保存時のサーバーサイド暗号化設定
* Enabled
    * `false`：デフォルト値。**AWS所有キー** が利用される
    * `true`：指定した **AWS(カスタマー)管理キー** が利用される
* SSEType：`KMS`が唯一サポートされている値
* KMSMasterKeyId：Enabled が `true` の場合に使用するキーをここで指定する

#### TableClass

* `STANDARD`：標準クラス
* `STANDARD_INFREQUENT_ACCESS`：低頻度アクセス
    * メリット：ストレージ料金が安くなる
    * デメリット：読み書き料金が高くなる
    * つまりは「大量データが登録されているがアクセス頻度が少ない」というケースに有用

#### DeletionProtectionEnabled

* 削除保護の有効化設定（CFnとかで事故ること多そうなので基本はON？）
* DynamoDB Local だとONにしてても削除できてしまう:cry:

#### Tags

* タグの設定


# DeleteTable

```ts
const command = new DeleteTableCommand({
  TableName: TABLE_NAME,
});
```

#### 基本

* テーブル削除

#### TableName（必須）


# ListTables

```ts
const command = new ListTablesCommand({
  ExclusiveStartTableName: 'OrderHistory',
  Limit: 10,
});
```

#### 基本

* テーブル一覧を取得

#### ExclusiveStartTableName

* 指定したテーブル名の次結果から返却するよう指定できる

#### Limit

* 返却されるテーブル名の最大数
* デフォルトは100

#### （まず使うことはないであろう） ExclusiveStartTableName 指定時の例

* 「OrderHistory・OrderHistory2・OrderHistory3」が存在
* ListTablesのLimitは1
* 全件取得が完了するまで処理を繰り返したい場合のコーディング例
    ```ts
    let exclusiveStart = undefined;
    // LastEvaluatedTableName が空になるまで続ける
    while (true) {
      try {
        const command = new ListTablesCommand({
          ExclusiveStartTableName: exclusiveStart,
          Limit: 1,
        } as ListTablesCommandInput);
        const output: ListTablesCommandOutput = await ddbClient.send(command);
        console.log('===================== listTables success =====================\n', JSON.stringify(output, null, 2));
    
        exclusiveStart = output.LastEvaluatedTableName || undefined;
        if (!exclusiveStart) break;
      } catch (err) {
        console.error('===================== listTables error =====================\n', err);
        break;
      }
    }
    ```
* 出力結果
    ```text
    ~/workspace/dynamodb-test (main) % npm run test
    
    > dynamodb-test@1.0.0 test
    > ./node_modules/.bin/ts-node ./src/dynamodb.ts
    
    ===================== listTables success =====================
     {
      "$metadata": {
        ...
      },
      "TableNames": [
        "OrderHistory"
      ],
      "LastEvaluatedTableName": "OrderHistory"
    }
    ===================== listTables success =====================
     {
      "$metadata": {
        ...
      },
      "TableNames": [
        "OrderHistory2"
      ],
      "LastEvaluatedTableName": "OrderHistory2"
    }
    ===================== listTables success =====================
     {
      "$metadata": {
        ...
      },
      "TableNames": [
        "OrderHistory3"
      ]
    }
    ```


# DescribeTable

```ts
const command = new DescribeTableCommand({
  TableName: TABLE_NAME,
});
```

#### 基本

* テーブル定義詳細情報を取得

#### TableName（必須）



# CRUD - 共通OPTION

|  | Put | Update | Delete | Get | Query | Batch<br>Get | Batch<br>Write |
|:-|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| ConditionExpression | ○ | ○ | ○ | - | - | - | - |
| ExpressionAttributeNames | ○ | ○ | ○ | ○ | ○ | ○ | - |
| ExpressionAttributeValues | ○ | ○ | ○ | - | ○ | - | - |
| ReturnValues | ○ | ○ | ○ | - | - | - | - |
| ReturnValuesOn<br>ConditionCheckFailure | ○ | ○ | ○ | - | - | - | - |
| ReturnItem<br>CollectionMetrics | ○ | ○ | ○ | - | - | - | ○ |
| ReturnConsumedCapacity | ○ | ○ | ○ | ○ | ○ | ○ | ○ |
| ConsistentRead | - | - | - | ○ | ○ | ○ | - |
| ProjectionExpression | - | - | - | ○ | ○ | ○ | - |

#### ConditionExpression

* [ Put | Update | Delete ] アクション実行時の条件式を設定できる（※ BatchWrite は不可）
* Putでの上書き防止・Updateでの非存在項目の追加防止が可能となる
* 条件式には　**比較演算子・論理式・関数** が使用可能
    * 比較演算子：= | `<>` | `<` | `>` | `<=` | `>=` | `BETWEEN` | `IN`
    * 論理式：`AND` | `OR` | `NOT`
    * 関数：以下の機能が提供されている

    | 関数 | 概要 |
    |:-|:-|
    | attribute_exists(*path*) | 項目にpathで指定した属性が含まれる場合 true |
    | attribute_not_exists(*path*) | pathで指定した属性が項目に存在しない場合true |
    | attribute_type(*path, type*) | pathが特定のデータ型である場合true<br>`type`の値には式の属性値を使用する必要あり |
    | contains(*path, operand*) | pathに「String, Set, List」のいずれかの属性値をもつ属性を指定<br>operandにはpathに指定した属性に対応する式の属性値を指定 |
    | begins_with(*path, substr*) | pathで指定された属性が`substr`に指定した値から始まる場合true |
    | size(*path*) | pathで指定した属性のサイズを表す数値を返す |

:::note 
**ConditionExpression の正しい理解**

初心者は挙動に混乱すると思われます。(...自分だけ？)
以下の記事がわかりやすいです。
参考：[Understanding DynamoDB Condition Expressions](https://www.alexdebrie.com/posts/dynamodb-condition-expressions/?utm_source=pocket_mylist)

条件式の評価は次の手順で行われるようです。
1. 最大1つの項目にマッチする同じ(複合)主キーを持つ既存項目を見つける
1. マッチした項目があれば、それに対してConditionExpressionを評価（既存項目がない場合はnullに対して評価）
1. trueと評価された場合、書き込みを実行

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3537959/634c4dc9-2dd9-8976-82ca-2c80e53f5c8c.png)

これは以下のように解釈でき、挙動もその通りになります。

```text
ExistingItem： { PK: 1, SK: 2 }
PutItem：      { PK: 1, SK: 3 }

ConditionExpression: attribute_not_exists(PK)

=> success
```

直感的にはPKが重複しているのでエラーになりそうですが、成功したので？？？でした。
理由は**手順１**で「put予定のItemと同様の複合主キーを持つ既存項目」を取得するためです。
例の場合 { PK: 1, SK: 3 } の組み合わせは既存テーブルに存在しません。
attribute_not_exists() は null に対して評価することになり true になるという仕組みです。

つまり、複合主キーを持つテーブルに対して
`attribute_not_exists(PK) AND attribute_not_exists(SK)`
という条件式は不要、ということになります。
PK・SK・PK+SKを指定したいずれの条件式も結果は同じになります。
:::

#### ExpressionAttributeNames

* 式内で使用する属性名の置換トークン
* 必須ではないが以下ユースケースの場合使用が推奨されているので基本は使うでいいはず
    * DynamoDB の予約語と名前が競合する属性（例：Percentile）
    * 式内で属性名が繰り返し出現するためのプレースホルダーを作成
    * 属性名の特殊文字が式内で誤って解釈されるのを防ぐ
* `#`を使用して属性名を逆参照する（例：**{ '#P': 'Price' }**）

#### ExpressionAttributeValues

* 式内で置換できる値
* `:`を使用して属性値を逆参照する（例：**{ ':P': 100 }**）

#### ReturnValues

* アクションにより指定可能な値が異なる
    |  | 説明 | Update | Put | Delete |
    |:-|:-|:-:|:-:|:-:|
    | NONE | デフォルト。値を何も返さない | ○ | ○ | ○ |
    | ALL_OLD | 更新前の全属性値を返却 | ○ | ○ | ○ |
    | ALL_NEW | 更新後の全属性値を返却 | ○ | - | - |
    | UPDATED_OLD | 更新前の更新対象属性値のみを返却 | ○ | - | - |
    | UPDATED_NEW | 更新後の更新対象属性値のみを返却 | ○ | - | - |


#### ReturnValuesOnConditionCheckFailure

* ConditionExpression で false となった場合のレスポンス設定を調整する
    * `NONE`：デフォルト値。何も返さない
    * `ALL_OLD`：テーブルにある該当 Item を返却する
        * 該当 Item 特定のために getItem でRCUを消費する必要がなくなる
        * DynamoDB Local だと response の Item が undefined になってしまう:cry:
        * ALL_OLD 指定時の返却内容
            ```text
            ConditionalCheckFailedException: The conditional request failed
                ...
            {
              '$fault': 'client',
              '$metadata': {
                httpStatusCode: 400,
                ...
              },
              Item: {
                Product: { S: 'apple' },
                Price: { N: '100' },
                Date: { S: '2023-11-01 00:00:01' },
                OrderNumber: { N: '1' },
                CustomerId: { S: 'A001' }
              }, // エラーの起因となったItem情報を返却してくれる
              __type: 'com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException'
            }
            ```

#### ReturnConsumedCapacity

* 消費されたRCU/WCU情報に関する詳細レベルを設定する
    * `NONE`：デフォルト値。集計に何も含めない
    * `TOTAL`：操作の集計のみが含まれる
    * `INDEXES`：アクセスされた各テーブル・セカンダリインデックスとともに、操作の集計が含まれる
    * TOTAL と INDEXES の出力差分
        ```text
        ===================== TOTAL =====================
          "ConsumedCapacity": {
            "CapacityUnits": 1,
            "TableName": "OrderHistory"
          }
        ===================== INDEXES =====================
          "ConsumedCapacity": {
            "CapacityUnits": 3,
            "GlobalSecondaryIndexes": {
              "GSI_1": {
                "CapacityUnits": 1
              }
            },
            "LocalSecondaryIndexes": {
              "LSI_1": {
                "CapacityUnits": 1
              }
            },
            "Table": {
              "CapacityUnits": 1
            },
            "TableName": "OrderHistory"
          }
        ```

#### ReturnItemCollectionMetrics

* `NONE`：デフォルト値。値を何も返さない
* `SIZE`：影響を受けた ItemCollection に関する以下の情報を返却する
    * ItemCollectionKey：PartitionKey の情報
    * SizeEstimateRangeGB：
        * ItemCollectionSize の下限/上限を含むGB単位の推定値(LSIに投影される属性サイズ含む)
        * LSI の10GB制限に近づいているか測定可能
        * 算出される数値はあくまで見積りで高い信頼性はない模様
    * DynamoDB Local だと設定が反映されない:cry:
    * SIZE 指定時の出力例
        ```text
          "ItemCollectionMetrics": {
            "ItemCollectionKey": {
              "CustomerId": "A001"
            },
            "SizeEstimateRangeGB": [
              0,
              1
            ]
          }
        ```

#### ConsistentRead

* `false`：デフォルト値。**結果整合性**での読み取りとなる
* `true`：**強い結果整合性**での読み取りとなる

#### ProjectionExpression

* RDB の SELECT句のようなもの
    * 指定しない場合：Itemの全属性が返却される（SELECT * FROM）
    * 指定する場合：
        * 指定したItemの属性が返却される（SELECT a, b FROM）
        * 複数項目指定する場合は１つの文字列内でカンマで区切る
* 指定あり/なしの出力差分（指定あり：CustomerId, Product を指定）
    ```text
    ===================== Option =====================
    
    ProjectionExpression: 'CustomerId, Product'
    
    ===================== Option指定なし =====================
    {
      "$metadata": {
        ...
      },
      "Item": {
        "Product": "apple",
        "OrderNumber": 1,
        "CustomerId": "A001",
        "Price": 100,
        "Timestamp": "2023-11-01 00:00:01"
      }
    }
    ===================== Option指定あり =====================
    {
      "$metadata": {
        ...
      },
      "Item": {
        "Product": "apple",
        "CustomerId": "A001"
      }
    }
    ```


# Put

```ts
// use DynamoDBDocumentClient
const command = new PutCommand({
  TableName: TABLE_NAME,
  Item: {
    CustomerId: 'A001',
    OrderNumber: 2,
    Product: 'pine',
    Price: 100,
    Date: '2023-11-01 00:00:01',
  },
  ConditionExpression: attribute_not_exists(CustomerId)
});
```
#### 基本

* Itemを1件追加・更新
    * 追加：主キー(複合主キー)が重複しない場合
    * 更新：主キー(複合主キー)が重複する場合
        * 更新を回避するには`ConditionExpression`で条件式を指定する

#### TableName（必須）

#### Item（必須）

* Item内は(複合)主キーが必須項目



# Update

```ts
// use DynamoDBDocumentClient
const command = new UpdateCommand({
  TableName: TABLE_NAME,
  Key: {
    CustomerId: 'A001',
    OrderNumber: 1,
  },
  ConditionExpression: attribute_exists(CustomerId),
  ExpressionAttributeNames: { '#P': 'Product' },
  ExpressionAttributeValues: { ':P': 'pine' },
  UpdateExpression: 'set #P = :P'
});
```

#### 基本

* Itemを1件更新・追加
    * 更新：Keyと一致するItemがある場合
    * 追加：Keyと一致するItemがない場合
        * 追加を回避するには`ConditionExpression`で条件式を指定する

#### TableName（必須）

#### Key（必須）

* Item内は(複合)主キーが必須項目

#### UpdateExpression

参考：[DynamoDBでデータを更新する際に使うUpdateExpressionについて一通りまとめてみた](https://dev.classmethod.jp/articles/dynamodb-update-expression-actions/)

* Update実行時のアクションを定義する
    * **`SET`**：1つ以上の項目を更新・追加する
        * 更新：指定した属性が既に存在する場合
        * 追加：指定した属性が存在しない場合
        * 次の組み込み関数をサポートしている
            * if_not_exists(*path, operand*)：属性の上書きを回避できる
            * list_append(*operand, operand*)：リストの結合ができる
        * 数値の加減算が可能（ただし加減算は`ADD`で行うことが推奨されている）
        * [ SET - UseCase1 ]：数値の加算と要素の追加
            ```text
            ExpressionAttributeNames: {
              '#P': 'Price',
              '#N': 'Note',
            },
            ExpressionAttributeValues: {
              ':P': 10,
              ':N': 'Price変更',
            },
            UpdateExpression: 'set #P = #P + :P, #N = :N',
            
            ===================== before =====================
            
              {
                "Product": "apple",
                "OrderNumber": 1,
                "CustomerId": "A001",
                "Price": 100,
                "Timestamp": "2023-11-01 00:00:01"
              }
            
            ===================== after =====================
            
              {
                "Product": "apple",
                "OrderNumber": 1,
                "CustomerId": "A001",
                "Price": 110, // 元の数値に加算される
                "Timestamp": "2023-11-01 00:00:01",
                "Note": "Price変更" // 属性が追加される
              }
            ```
            <br>
        * [ SET - UseCase2 ]：if_not_exists() で上書き防止
            ```text
            ExpressionAttributeNames: {
              '#P': 'Price',
              '#N': 'Note',
            },
            ExpressionAttributeValues: {
              ':P': 200,
              ':N': '属性追加',
            },
            UpdateExpression: 'set #P = if_not_exists(#P, :P), #N = if_not_exists(#N, :N)',
            
            ===================== before =====================
            
              {
                "Product": "apple",
                "OrderNumber": 1,
                "CustomerId": "A001",
                "Price": 100,
                "Timestamp": "2023-11-01 00:00:01"
              }
            
            ===================== after =====================
            
              {
                "Product": "apple",
                "OrderNumber": 1,
                "CustomerId": "A001",
                "Price": 100, // Price属性は既に存在していたため上書き防止される
                "Timestamp": "2023-11-01 00:00:01",
                "Note": "属性追加"
              }
            ```
            <br>
        *  [ SET - UseCase3 ]：list_append() でリストを結合
            ```text
            ExpressionAttributeNames: { '#P': 'PriceList' },
            ExpressionAttributeValues: { ':P': [300, 400] },
            UpdateExpression: 'set #P = list_append(#P, :P)',
            
            ===================== before =====================
            
              {
                "PriceList": [
                  100,
                  200
                ],
                "OrderNumber": 1,
                "CustomerId": "A001"
              }
            
            ===================== after =====================
            
              {
                "PriceList": [
                  100,
                  200,
                  300,
                  400
                ], // 元リストと結合される
                "OrderNumber": 1,
                "CustomerId": "A001"
              }
            ```
            <br>
        *  [ SET - UseCase4 ]：マップ要素とリスト要素にアクセスする
            ```text
            ExpressionAttributeNames: {
              '#PM': 'ProductMap',
              '#F1': 'fruits1',
              '#PMF1': 'ProductMap.fruits1',  // (※1)マップ要素に対しこのアクセスの仕方はできない
              '#PL': 'PriceList',
              '#PL0': 'PriceList[0]',  // (※2)リスト要素に対しこのアクセスの仕方はできない
            },
            ExpressionAttributeValues: {
              ':PMF1': 'grape',
              ':PL0': 300,
            },
            UpdateExpression: 'set #PM.#F1 = :PMF1, #PMF1 = :PMF1, #PL[0] = :PL0, #PL0 = :PL0',
            
            ===================== before =====================
            
              {
                "OrderNumber": 1,
                "CustomerId": "A001",
                "ProductMap": {
                  "fruits1": "apple",
                  "fruits2": "pine"
                },
                "PriceList": [
                  100,
                  200
                ]
              }
            
            ===================== after =====================
            
              "Attributes": {
                "PriceList[0]": 300, // (※2)新しい属性として追加されてしまう
                "OrderNumber": 1,
                "ProductMap": {
                  "fruits1": "grape", // '#PM.#F1'でマップ要素にアクセスできていることがわかる
                  "fruits2": "pine"
                },
                "ProductMap.fruits1": "grape",  // (※1)新しい属性として追加されてしまう
                "CustomerId": "A001",
                "PriceList": [
                  300, // '#PL[0]'でリスト要素にアクセスできていることがわかる
                  200
                ]
              }
            ```
            <br>
    * **`REMOVE`**：アイテムから1つ以上の属性を削除する
        * マップやリスト内の要素単位で削除も可能
        * [ REMOVE - UseCase1 ]：属性およびリスト内の要素削除
            ```text
            ExpressionAttributeNames: {
              '#N': 'Note',
              '#PM': 'ProductMap',
              '#F1': 'fruits1',
              '#PL': 'PriceList',
            },
            UpdateExpression: 'remove #N, #PM.#F1, #PL[0]',
            
            ===================== before =====================
            
              {
                "OrderNumber": 1,
                "CustomerId": "A001",
                "ProductMap": {
                  "fruits1": "削除対象",
                  "fruits2": "pine"
                },
                "PriceList": [
                  100,
                  200
                ],
                "Note": "削除対象"
              }
            
            ===================== after =====================
            
             {
                "OrderNumber": 1,
                "ProductMap": {
                  "fruits2": "pine"
                },
                "CustomerId": "A001",
                "PriceList": [
                  200
                ]
              }
            ```
            <br>
    * **`ADD`**：属性を追加する
        * 数値型(N)かセット型(NS/SS/BS)へのアクションのみ許可
            * 指定した属性が存在しない場合：指定された属性と共に値を追加
            * 指定した属性が存在する場合：
                * 数値型(N)：加減算ができる
                * セット型(NS/SS/BS)：要素の追加ができる
        * [ ADD - UseCase1 ]：数値型の加算(属性追加)・減算
            ```text
            ExpressionAttributeNames: {
              '#P': 'Price',
              '#P2': 'Price2',
            },
            ExpressionAttributeValues: {
              ':P': -100,
              ':P2': 200,
            },
            UpdateExpression: 'add #P :P, #P2 :P2',
            
            ===================== before =====================
            
              {
                "Price": 100,
                "OrderNumber": 1,
                "CustomerId": "A001"
              }
            
            ===================== after =====================
            
              {
                "Price2": 200,
                "OrderNumber": 1,
                "CustomerId": "A001",
                "Price": 0
              }
            ```
            <br>
        * [ ADD - UseCase2 ]：セット型の追加
            ```text
            ExpressionAttributeNames: { '#P': 'PriceSet' },
            ExpressionAttributeValues: { ':P': new Set([300]) },
            UpdateExpression: 'add #P :P',
            
            ===================== before =====================
            
              { OrderNumber: 1, PriceSet: Set(2) { 100, 200 }, CustomerId: 'A001' }
            
            ===================== after =====================
            
              {
                OrderNumber: 1,
                PriceSet: Set(3) { 100, 200, 300 },
                CustomerId: 'A001'
              }
            ```

            <br>
            
    * **`DELETE`**：指定した値をデータから削除する
        * セット型のデータのみサポート
        * DELETEアクションにより要素が空になると属性ごと消える（セット型は空の状態が許容されないため）
        * [ DELETE - UseCase1 ]：セット型の要素削除
            ```text
            ExpressionAttributeNames: {
              '#P': 'PriceSet',
              '#P2': 'PriceSet2',
            },
            ExpressionAttributeValues: {
              ':P': new Set([200]),
              ':P2': new Set([400]),
            },
            UpdateExpression: 'delete #P :P, #P2 :P2',
            
            ===================== before =====================
            
              {
                OrderNumber: 1,
                PriceSet: Set(2) { 100, 200 },
                CustomerId: 'A001',
                PriceSet2: Set(1) { 400 }
              }
            
            ===================== after =====================
            
              { OrderNumber: 1, PriceSet: Set(1) { 100 }, CustomerId: 'A001' }
            ```
    :::note
    **セット型について**
    
    上記ユースケースにある通り、例えばjavascriptでは`new Set([...])`で明示的にSet型を作成する必要があります。
    （純粋なDynamoDBClientであればNS等でセット型を指定できそうですが）
    また、セット型を`JSON.Stringfy()`すると`{}`で出力されてしまい、リストやオブジェクトとは異なる挙動をします。
    ・型縛り
    ・重複排除
    などがセット型の特性です。（この強力な制約がどうしても必要、というケース以外では混乱を招く可能性高し...？）



# Delete

```ts
// use DynamoDBDocumentClient
const command = new DeleteCommand({
  TableName: TABLE_NAME,
  Key: {
    CustomerId: 'A001',
    OrderNumber: 1,
  }
});
```

#### 基本

* Itemを1件削除

#### TableName（必須）

#### Key（必須）

* Item内は(複合)主キーが必須項目



# Get

```ts
// use DynamoDBDocumentClient
const command = new GetCommand({
  TableName: TABLE_NAME,
  Key: {
    CustomerId: 'A001',
    OrderNumber: 1,
  },
});
```

#### 基本

* Itemを1件取得

#### TableName（必須）

#### Key（必須）

* (複合)主キーの指定が必須
    * 一意の Item を取得するコマンドなので、例えば複合主キーのテーブルに対しPartitionKeyのみ指定は不可



# Query

```ts
// use DynamoDBDocumentClient
const command = new QueryCommand({
  TableName: TABLE_NAME,
  // ExclusiveStartKey: {
  //   CustomerId: 'A001',
  //   OrderNumber: 1,
  // },
  ExpressionAttributeNames: {
    '#C': 'CustomerId',
    '#P': 'Price',
  },
  ExpressionAttributeValues: {
    ':C': 'A001',
    ':P': 100,
  },
  FilterExpression: '#P > :P',
  // IndexName: 'GSI_1',
  KeyConditionExpression: '#C = :C',
  ProjectionExpression: 'CustomerId, OrderNumber',
  Limit: 3,
  ScanIndexForward: true, // || false
  // Select: 'ALL_ATTRIBUTES', // || ALL_PROJECTED_ATTRIBUTES || COUNT || SPECIFIC_ATTRIBUTES
});

```

#### 基本

* RDB の SELECT に近い
* PKは必ず指定する必要があるため、`select * from table_name;`のような全件取得はできない。
* 全件取得は`Scan`を使う
* SQL互換のクエリ言語を使いたければ[PartiQL](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/ql-reference.html)という機能もサポートされている

#### TableName（必須）

#### KeyConditionExpression（必須）

* PartitionKeyの指定が必須
* 式内で使用する値は ExpressionAttributeValues を用いた置換が必要
* SortKey も存在する場合は`AND`条件で絞り込みが可能
    * 比較演算子`<>`は使用不可
    * `BETWEEN :val1 AND :val2` と `begins_with( sortKeyName, :val)` を使用可能

#### ExclusiveStartKey

* 指定した(複合)主キーの次のItemから取得を開始する
* `LastEvaluatedKey`と組み合わせて使う

#### FilterExpression

* (複合)主キー以外の絞り込みが必要な場合に使用する
* 使用できる構文は`ConditionExpression`と同じ
* [ FilterExpression - UseCase1 ]：特定の属性を持つItemをFilter
    ```text
    ===================== data =====================
    
    { CustomerId: 'A001', OrderNumber: 1, Price: 100 }
    { CustomerId: 'A001', OrderNumber: 2 }
    { CustomerId: 'A001', OrderNumber: 3 }
    
    ExpressionAttributeNames: { '#C': 'CustomerId' },
    ExpressionAttributeValues: { ':C': 'A001' },
    KeyConditionExpression: '#C = :C',
    ProjectionExpression: 'CustomerId, OrderNumber',
    
    FilterExpression: 'attribute_exists(Price)' // 'Price'属性を持つItemのみFilterする
    
    ===================== FilterExpressionなし =====================
    
      [
        {
          "OrderNumber": 1,
          "CustomerId": "A001"
        },
        {
          "OrderNumber": 2,
          "CustomerId": "A001"
        },
        {
          "OrderNumber": 3,
          "CustomerId": "A001"
        }
      ]
    
    ===================== FilterExpressionあり =====================
    
      [
        {
          "OrderNumber": 1,
          "CustomerId": "A001"
        }
      ]
    ```

#### IndexName

* 使用するGSI/LSIのINDEX名を指定
    * DynamoDB では RDB の EXPLAIN のようなことはしてくれず、明示的に指定の必要がある模様
    * RDB だと FORCE INDEX句
* GSI指定の場合、KeyConditionExpression に指定するPKはもちろんGSI対応のもの
* デフォルトではテーブルINDEXが使用される

#### Limit

* 取得する最大数を指定
* RDB だと Limit句
* Limit上限に到達した場合、`LastEvaluatedKey`が返却される
    * `ExclusiveStartKey`と組み合わせて途中から読み込みを再開できる

#### ScanIndexForward

* SortKeyのソート順を決める
    * `true`：昇順。デフォルト値
    * `false`：降順
* RDB だと ORDER BY句

#### Select

* ProjectionExpression と同様、RDB の SELECT句
* 以下のように抽出対象の抽象的な指定ができる
    * `ALL_ATTRIBUTES`：すべての属性を取得
    * `ALL_PROJECTED_ATTRIBUTES`：INDEXに投影されたすべての属性を取得
    * `COUNT`：一致するアイテムの数を返す。RDB の `select COUNT(a) FROM`
    * `SPECIFIC_ATTRIBUTES`：ProjectionExpression 指定属性のみ返却
* `SPECIFIC_ATTRIBUTES`以外 ProjectionExpression との併用不可
* ProjectionExpression があれば`COUNT`以外このオプション要らないのでは...？
* ただ、このオプションを使うことでスループットコストの最適化ができそうなこともあったので、とりあえず頭には入れておく

# Scan

```ts
// use DynamoDBDocumentClient
const command = new ScanCommand({
  TableName: TABLE_NAME,
});
```

#### 基本

* テーブルデータを全件取得

#### TableName（必須）



# BatchGet

```ts
// use DynamoDBDocumentClient
const command = new BatchGetCommand({
  RequestItems: {
    [TABLE_NAME]: {
      Keys: [
        { CustomerId: 'A001', OrderNumber: 1 },
        { CustomerId: 'A001', OrderNumber: 2 },
      ],
      ProjectionExpression: 'CustomerId, Product',
    },
    [TABLE_NAME_2]: {
      Keys: [
        { CustomerId: 'A001', OrderNumber: 1 },
        { CustomerId: 'A001', OrderNumber: 2 },
      ],
      ProjectionExpression: 'CustomerId, Product',
    },
  },
  ReturnConsumedCapacity: 'NONE'
});
```

#### 基本

* 1つ以上のテーブルから 1つ以上の項目を取得できる
* 1回のオペレーションで取得できるデータにはいくつか制約が存在する
    * 合計で最大**16MB**のデータまで
    * Itemは最大で**100個**まで

#### RequestItems（必須）

* 「1つ以上のテーブル」から「1つ以上の項目を取得」するためのマップ
* テーブル名単位で要素を指定する
    * `Keys`に Item の(複合)主キーを指定する
    * その他オプションは getItem と同様



# BatchWrite

```ts
// use DynamoDBDocumentClient
const command = new BatchWriteCommand({
  RequestItems: {
    [TABLE_NAME]: [
      {
        PutRequest: {
          Item: { CustomerId: 'A001', OrderNumber: 1 },
        },
      },
      {
        DeleteRequest: {
          Key: { CustomerId: 'A001', OrderNumber: 2 },
        },
      },
    ],
    [TABLE_NAME_2]: [
      {
        PutRequest: {
          Item: { CustomerId: 'A001', OrderNumber: 1 },
        },
      },
      {
        DeleteRequest: {
          Key: { CustomerId: 'A001', OrderNumber: 2 },
        },
      },
    ],
  },
  ReturnConsumedCapacity: 'INDEXES',
  ReturnItemCollectionMetrics: 'SIZE',
});
```

#### 基本

* 1つ以上のテーブルから 1つ以上の項目を追加・削除できる
* 1回のオペレーションで書き込みできるデータにはいくつか制約が存在する
    * 合計で最大**16MB**のデータまで
    * Itemは最大で**25個**まで
* 追加・削除をまとめて実行することができる

#### RequestItems（必須）

* 「1つ以上のテーブル」から「1つ以上の項目を追加・削除」するためのマップ
* テーブル名単位で要素を指定する
    * `PutRequest`
        * Put オペレーションを実行する場合に指定
        * PutRequest 内は Put オペレーション時と同様`Item`に(複合)主キーを指定する
        * ConditionExpression が指定できないことから**上書き操作を回避できないことに注意**
    * `DeleteRequest`
        * Delete オペレーションを実行する場合に指定
        * DeleteRequest 内は Delete オペレーション時と同様`Key`に(複合)主キーを指定する

:::note 
**[ BatchGet - UnprocessedKeys ] / [ BatchWrite - UnprocessedItems ]について**

BatchGet/BatchWrite ではスループットやレスポンスサイズの超過で一部の処理しか成功しなかった場合、成功した処理はロールバックされないようです。
上記ケースの場合、レスポンスの `UnprocessedKeys` または `UnprocessedItems` 内に未処理の Key/Item が格納されてきます。

全ての処理を正常に完了させたい場合、1回のオペレーションで処理できる上限と合わせて以下のような考慮をした実装が必要になってきそうです。
（複数テーブル取得を考慮するともうちょっと考えないといけない）

例：BatchGet で UnprocessedKeys を考慮する（参考：[AWS SDK for JavaScript: DynamoDB batchGet/batchWriteのサンプル](https://qiita.com/aosho235/items/20eba977d3ee50504e27)）

スニペット
```ts
// 125個のItemをBatchGetする
const sampleKeys = [] as any;
for (let i = 1; i <= 125; i++) {
  const key = { CustomerId: 'A001', OrderNumber: i };
  sampleKeys.push(key);
}

/***************************************
  batch get item
***************************************/
const batchGetItem = async () => {
  try {
    const result = [];
    const limit = 100; // 1回のbatchGet操作におけるItemの取得上限
    let unprocessedKeys = sampleKeys.slice();

    while (unprocessedKeys.length > 0) {
      const requestKeys = unprocessedKeys.splice(0, limit); // 取得上限までrequestKeysに移植
      const command = new BatchGetCommand({
        RequestItems: {
          [TABLE_NAME]: {
            Keys: requestKeys,
          },
        },
      } as BatchGetCommandInput);

      const output: BatchGetCommandOutput = await ddbDocClient.send(command);
      const res = output?.Responses?.[TABLE_NAME];
      const unprocessed = output?.UnprocessedKeys?.[TABLE_NAME]?.Keys;

      if (res) result.push(...res); // 取得完了分をresultへpush
      if (unprocessed) unprocessedKeys.push(...unprocessed); // 未処理分をunprocessedKeysへpush
    } // unprocessedKeysが0になるまで繰り返し

    console.log('===================== batchGetItem success =====================\n', JSON.stringify(result, null, 2), '\ncount: ', result.length);
    return result;
  } catch (err) {
    console.error('===================== batchGetItem error =====================\n', err);
  }
};
```
:::


# 参考
* https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html
* https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Introduction.html
* https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/dynamodb/
* https://maku.blog/p/5mv5dkt/
* https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html