DynamoDB を触ってみる

## 初めに

https://qiita.com/tkek321/items/a86ccc21908a5836c472

この記事で DynamoDB について調べたので、せっかくなら触ってみる。

## ローカル環境構築

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html#docker

```yaml
version: '3.8'
services:
 dynamodb-local:
   command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data"
   image: "amazon/dynamodb-local:latest"
   container_name: dynamodb-local
   ports:
     - "8000:8111" # 公式では 8000 ですが、port が被ってたので 8111
   volumes:
     - "./docker/dynamodb:/home/dynamodblocal/data"
   working_dir: /home/dynamodblocal
```

aws cli でアクセスできる

```bash
aws dynamodb list-tables --endpoint-url http://localhost:8111
{
    "TableNames": []
}
```

## テーブル作成

X のような SNS を想定した次のような機能があるシステムを想定。

```
1. ユーザーのプロフィール管理

ユーザー名、メールアドレス、登録日などの基本情報を保存。
ユーザー間の友達リスト

2. 各ユーザーが持つ友達リストを保存。
投稿（ポスト）機能

3. ユーザーが投稿を作成する。
各投稿にはタイトル、本文、作成日時、いいね数が含まれる。
いいね（Likes）機能

4. 各投稿に対して他のユーザーが「いいね」できる。
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364773/4554eef8-deb6-359d-7c9d-bef356130639.png)


### テーブル定義の json を記載する

```json
{
  "TableName": "ExampleSns",
  "AttributeDefinitions": [
    {
      "AttributeName": "PK",
      "AttributeType": "S"
    },
    {
      "AttributeName": "SK",
      "AttributeType": "S"
    }
  ],
  "KeySchema": [
    {
      "AttributeName": "PK",
      "KeyType": "HASH"
    },
    {
      "AttributeName": "SK",
      "KeyType": "RANGE"
    }
  ],
  "ProvisionedThroughput": {
    "ReadCapacityUnits": 5,
    "WriteCapacityUnits": 5
  },
  "GlobalSecondaryIndexes": [
    {
      "IndexName": "GSI1",
      "KeySchema": [
        {
          "AttributeName": "SK",
          "KeyType": "HASH"
        },
        {
          "AttributeName": "PK",
          "KeyType": "RANGE"
        }
      ],
      "Projection": {
        "ProjectionType": "ALL"
      },
      "ProvisionedThroughput": {
        "ReadCapacityUnits": 5,
        "WriteCapacityUnits": 5
      }
    }
  ]
}

```

- TableName
    - テーブル名
- AttributeDefinitions
    - テーブルとインデックスのキースキーマを記述する属性のリスト
    - AttributeType
        -  `N` は数値、`S` は文字列、`B` はバイナリ
- KeySchema
    - "KeyType": "HASH" ... パーティションキー
    -  "KeyType": "RANGE" ... ソートキー
-  ProvisionedThroughput
    - RCU
    - WCU
- GlobalSecondaryIndex
    - グローバルセカンダリーインデックスの定義
- IndexName
    - インデックス名
- KeySchema
    - インデックスが使用するキーを定義
    - GSI は同じデータでもう１つ別のテーブルを作成するようなイメージ
    - SK に GSI を設定することは必須でもなんでもないが、設定するケースは多そうかも？
    - 例えば、投稿日時（SK）でソートされた投稿を取得するようなクエリが実行可能になったりするらしい
- ProjectionType
    - GSI に含める属性を指定
    - https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-table-projection.html
- ProvisionedThroughput
    - GSI の読み取り・書き込みキャパシティユニットを設定
    - キャパシティユニットはテーブル、インデックスごとに設定する必要があるらしい


### テーブル作成

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/getting-started-step-1.html

```bash
aws dynamodb create-table \
    --cli-input-json file://table-definition.json \
    --endpoint-url http://localhost:8111
```

### 作成したテーブルの確認

```bash
aws dynamodb list-tables --endpoint-url http://localhost:8111
```
結果

```json
{
    "TableNames": [
        "ExampleSns"
    ]
}
```

## データの新規追加

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/getting-started-step-2.html

```bash
aws dynamodb put-item \
    --table-name ExampleSns \
    --item '{
        "PK": {"S": "user#123"},
        "SK": {"S": "profile"},
        "name": {"S": "A-san"},
        "email": {"S": "a-san@example.com"},
        "joined_at": {"S": "2024-01-01"}
    }' \
    --endpoint-url http://localhost:8111

```

### バッチ

一度に複数のクエリをまとめて投げることも可能。

ネットワークコールの回数を減らし効率化できる。

#### バッチを使った更新 `batch-write-item`

`batch-write-item` 

BatchWriteItem操作は、最大25個のPutItemまたはDeleteItem要求を一度に処理できる。


https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html

```bash
aws dynamodb batch-write-item \
    --request-items '{
        "ExampleSns": [
            {
                "PutRequest": {
                    "Item": {
                        "PK": {"S": "user#123"},
                        "SK": {"S": "post#2024-12-25T10:00"},
                        "content": {"S": "Merry Christmas!"},
                        "likes": {"N": "0"},
                        "comments": {"N": "0"}
                    }
                }
            },
            {
                "PutRequest": {
                    "Item": {
                        "PK": {"S": "user#123"},
                        "SK": {"S": "following#789"},
                        "followed_user_id": {"S": "user#789"},
                        "followed_at": {"S": "2024-12-24T09:00"}
                    }
                }
            },
            {
                "PutRequest": {
                    "Item": {
                        "PK": {"S": "user#789"},
                        "SK": {"S": "profile"},
                        "name": {"S": "B-san"},
                        "email": {"S": "b-san@example.com"},
                        "joined_at": {"S": "2023-01-01"}
                    }
                }
            }
        ]
    }' \
    --endpoint-url http://localhost:8111
```

結果

```json
{
    "UnprocessedItems": {}
}
```


バッチ処理はトランザクション処理ではないので、１つのクエリだけ失敗する場合もある。

`UnprocessedItems` に失敗したデータが入るらしいので、アプリケーション側での制御が必要。


## データを読み込む

### query コマンド

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/getting-started-step-3.html

ここではユーザーの profile 情報を取得する。

```bash
aws dynamodb query \
    --table-name ExampleSns \
    --key-condition-expression "PK = :userId AND SK = :sortKeyValue" \
    --expression-attribute-values '{":userId": {"S": "user#123"}, ":sortKeyValue": {"S": "profile"}}' \
    --endpoint-url http://localhost:8111
```

(AttributeName を `PK`, `SK` にしたせいでちょっとわかりにくくなってしまった...)

- `--table-name` 
    - ExampleSns テーブルが検索対象
- `--key-condition-expression`
    - 検索条件
        - `PK` （パーティションキーにつけている名前）の値が `:userId` 
        - かつ
        - `SK` （ソートキーにつけている名前）の値が `sortKeyValue`
- `--expression-attribute-values`
    - 検索する値
        - `:userId` に文字列 `user#123` を代入
        - `:sortKeyValue` に文字列 `profile` を代入
- `--endpoint-url`
    - エンドポイント URL

結果

```json
{
    "Items": [
        {
            "SK": {
                "S": "profile"
            },
            "name": {
                "S": "A-san"
            },
            "joined_at": {
                "S": "2024-01-01"
            },
            "PK": {
                "S": "user#123"
            },
            "email": {
                "S": "a-san@example.com"
            }
        }
    ],
    "Count": 1,
    "ScannedCount": 1,
    "ConsumedCapacity": null
}
```

### scan コマンド

特定のパーティションキーに依存しない全件取得。
取得が遅くなる。RCU の消費が大きくなるため基本的には使用しないのが無難。

```bash
aws dynamodb scan \
    --table-name ExampleSns \
    --endpoint-url http://localhost:8111
```

結果

```json
{
    "Items": [
        {
            "SK": {
                "S": "profile"
            },
            "name": {
                "S": "A-san"
            },
            "joined_at": {
                "S": "2024-01-01"
            },
            "PK": {
                "S": "user#123"
            },
            "email": {
                "S": "a-san@example.com"
            }
        }
    ],
    "Count": 1,
    "ScannedCount": 1,
    "ConsumedCapacity": null
}
```

### バッチを使った取得処理 `batch-get-item`

```bash
aws dynamodb batch-get-item \
    --request-items '{
        "ExampleSns": {
            "Keys": [
                {"PK": {"S": "user#123"}, "SK": {"S": "post#2024-12-25T10:00"}},
                {"PK": {"S": "user#123"}, "SK": {"S": "following#789"}},
                {"PK": {"S": "user#789"}, "SK": {"S": "profile"}}
            ]
        }
    }' \
    --endpoint-url http://localhost:8111
```

### GSIを利用した検索も可能

```bash
aws dynamodb query \
    --table-name ExampleSns \
    --index-name GSI1 \
    --key-condition-expression "SK = :sk" \
    --expression-attribute-values '{
        ":sk": {"S": "post#2024-12-25T10:00"}
    }' \
    --endpoint-url http://localhost:8111
```


## データの更新

### UpdateItem

投稿の`いいね`を増やす

```bash
aws dynamodb update-item \
    --table-name ExampleSns \
    --key '{"PK": {"S": "user#123"}, "SK": {"S": "post#2024-12-25T10:00"}}' \
    --update-expression "SET likes = likes + :inc" \
    --expression-attribute-values '{":inc": {"N": "1"}}' \
    --endpoint-url http://localhost:8111
```

- 対象キー (PK, SK): user#123 の投稿 post#2024-12-25T10:00
- 更新内容: likes 属性を +1
- 単一アイテムに対しての簡単な更新操作

### トランザクションを利用した更新処理 `TransactWriteItems`


```bash
aws dynamodb transact-write-items \
    --transact-items '[
        {
            "Update": {
                "TableName": "ExampleSns",
                "Key": {
                    "PK": {"S": "user#123"},
                    "SK": {"S": "profile"}
                },
                "UpdateExpression": "SET email = :email",
                "ExpressionAttributeValues": {":email": {"S": "hoge@example.com"}}
            }
        },
        {
            "Update": {
                "TableName": "ExampleSns",
                "Key": {
                    "PK": {"S": "user#123"},
                    "SK": {"S": "post#2024-12-25T10:00"}
                },
                "UpdateExpression": "SET likes = likes + :inc",
                "ExpressionAttributeValues": {":inc": {"N": "1"}}
            }
        }
    ]' \
    --endpoint-url http://localhost:8111


```



## データの削除

```bash
aws dynamodb delete-item \
    --table-name ExampleSns \
    --key '{"PK": {"S": "user#123"}, "SK": {"S": "post#2024-12-25T10:00"}}' \
    --endpoint-url http://localhost:8111
```

### 条件つきの削除

`いいね` が 0 件の場合のみ削除する

```bash
aws dynamodb delete-item \
    --table-name ExampleSns \
    --key '{"PK": {"S": "user#123"}, "SK": {"S": "post#2024-12-25T10:00"}}' \
    --condition-expression "likes = :zero" \
    --expression-attribute-values '{":zero": {"N": "0"}}' \
    --endpoint-url http://localhost:8111

```