DynamoDB の Get/Put/Query/Scan/Update Command のサンプル

# 背景
DynamoDB を使う為の、よく使うコマンドの操作例と、起きたエラーの対処などの記録

- GetCommand
- PutCommand
- QueryCommand
- ScanCommand
- UpdateCommand

# 利用例
## 前提
### AWS SDK V3 に記載のある Client ファイルを使う

https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-lib-dynamodb/#constructor

以下 ２ファイルを利用

:::note info
プロキシを利用する場合は、DynamoDBClient のところで設定しておく
:::

```typescript: ddbClient.ts
// Create the DynamoDB service client module using ES6 syntax.
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { NodeHttpHandler } from '@aws-sdk/node-http-handler';
// import { HttpsProxyAgent } from 'hpagent';
// const httpsAgent = new HttpsProxyAgent({ proxy: 'http://10.20.30.40:8080' });

// Set the AWS Region.
export const REGION = "ap-northeast-1";
// Create an Amazon DynamoDB service client object.
export const ddbClient = new DynamoDBClient({ 
    region: REGION,
    // requestHandler: new NodeHttpHandler({
    //     httpAgent: httpsAgent,
    //     httpsAgent: httpsAgent
    // })
});
```

```typescript: ddbDocClient.ts
// Create a service client module using ES6 syntax.
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { ddbClient } from "./ddbClient";
const marshallOptions = {
  // Whether to automatically convert empty strings, blobs, and sets to `null`.
  convertEmptyValues: false, // false, by default.
  // Whether to remove undefined values while marshalling.
  removeUndefinedValues: false, // false, by default.
  // Whether to convert typeof object to map attribute.
  convertClassInstanceToMap: false, // false, by default.
};
const unmarshallOptions = {
  // Whether to return numbers as a string instead of converting them to native JavaScript numbers.
  wrapNumbers: false, // false, by default.
};
const translateConfig = { marshallOptions, unmarshallOptions };
// Create the DynamoDB document client.
const ddbDocClient = DynamoDBDocumentClient.from(ddbClient, translateConfig);
export { ddbDocClient };
```

### テーブル
こんな感じのテーブルに対して
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120072/a0f17944-a90c-0ad0-7382-9fbb187bd27b.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120072/a6719569-84be-b93b-0e2d-55e1a875a8ee.png)

## Tips
### Sort: 昇順・降順
以下のように ScanIndexForward 使えばOK。
```typescript: sort
  const queryCommandInput = {
    Limit: 1, // 取得数。like "TOP"
    ScanIndexForward: false, // false: DESC, true: ASC 
  };
```

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Query.html

### 属性が格納されてるか？の確認方法
以下注意
- attribute_exists()
    - id 入ってるか？で見ると、消した場合に、(empty) として、属性は存在しているので反応してしまう
    - Artist/Enterpreneur みたいに、複数の Table を単一Tableで保存する場合に、特定の属性があるか？を見るために使うのかな
- size()
    - 対象属性の内容が一定文字数以上、とかで調べられるので、こちらが良さそう

### stream/db
- SDK.libClient で取得する場合は、低層APIを wrap してるので、気にしなくてOK
- Stream から取得した場合、以下のように型意識が必要<br><code>Number(dynamodb.Keys.ID.N ?? 0)</code><br><code>Boolean(dynamodb.Keys.IsVaild.BOOL)</code>
    - 途中で取得すると、string

## PutCommand
Sort Key の属性名の "#" を入れてるけど、以下な感じで文字列化してやれば問題なく行けた。
同一キーに対しては上書きなので、特定の属性だけ上書きしたい場合には、UpdateCommand を

```typescript: PutCommand
const putRecord = async (date: Date, postID: number) => {
    const timestamp = date.getTime();
    const params = {
        TableName: "TestCompositeKey",
        Item: {
            uid: "user02",
            "timestamp#post_id": `${timestamp}#${postID}`,
            timestamp: timestamp,
            timestampString: date.toLocaleString(),
            post_id: postID,
        },

    };
    const result = await ddbDocClient.send(new PutCommand(params));
}
```

取得する際に困ったときは以下

https://qiita.com/baku2san/items/e01662975d09aee2243e

## UpdateCommand
Put だと Key が同じ場合に完全に置換されてしまうので、
一部属性値の置換や追加が行いたい時はこちらを使う

```typescript: Update Example
const updateItem = async (content: any) => {
    const baseParams = {
        TableName: "TestCompositeKey",
        Key: {
            uid: content.PK,
            "timestamp#post_id": content.SK,
        },
        ExpressionAttributeNames: {
            "#data": "data",
        },
        ExpressionAttributeValues: {
            ":data": content.data,
        },
        UpdateExpression: `SET #data = :data ${(content.data2 ? ", #data2=:data2" : "")}`,
    };
    const mergeData2 = {
        ExpressionAttributeNames: {
            "#data2": "data2",
        },
        ExpressionAttributeValues: {
            ":data2": content.data2
        },
    };

    const params = (content.data2) ?
        _.merge(baseParams, mergeData2) :
        baseParams;
    return await ddbDocClient.send(new UpdateCommand(params));
};
```

上の例の詳細は以下で

https://qiita.com/baku2san/items/496f8506356eebda5062

これだけだと、動的といっても想定のプロパティによって切り替えるだけなので、以下で動的プロパティの場合を追記

### 動的に渡された項目だけの更新式を生成させる

https://qiita.com/baku2san/items/4a472afe8f479279cb1c

## GetCommand
PutCommand と大して変わらないので、特に問題無し

```typescript: GetCommand
const getRecord = async (PK: string, compositeSK: string) => {
    const params = {
        TableName: "TestCompositeKey",
        Key: {
            uid: PK,
            "timestamp#post_id": compositeSK,
        },
    }
    return (await ddbDocClient.send(new GetCommand(params))).Item;
}
```
## ScanCommand
基本全部取るのが Scan だけど、今回は Filter をかけて取得した例

:::note info
以下、注意

> フィルタ式は、Scan の完了後、結果が返される前に適用されます。そのため、Scan は、フィルタ式があるかどうかにかかわらず、同じ量の読み込みキャパシティーを消費します。

:::
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Scan.html

```typescript: ScanCommand
const scanRecords = async (startDate: Date, endDate: Date) => {
    const params = {
        TableName: "TestCompositeKey",
        ExpressionAttributeNames: {
            "#timestamp": "timestamp"
        },
        ExpressionAttributeValues: {
            // ":PK": userID,
            ":sortkeyval1": startDate.getTime(),
            ":sortkeyval2": endDate.getTime()
        },
        FilterExpression: "#timestamp BETWEEN :sortkeyval1 AND :sortkeyval2"
    }
    const usersGotten = (await ddbDocClient.send(new ScanCommand(params))).Items ?? [];
    return usersGotten;
}
```
https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#scan-property

### 属性が格納されてるか？で見落としがち
- attribute_exists()
    - id 入ってるか？で見ると、消した場合に、(empty) として、属性は存在しているので反応してしまう
    - Artist/Enterpreneur みたいに、複数の Table を単一Tableで保存する場合に、特定の属性があるか？を見るために使うのかな
- size()
    - 対象属性の内容が一定文字数以上、とかで調べられるので、こちらが良さそう<br><code>size( #token ) >= :tokenLength"</code>

## QueryCommand
インデックスを設定した場合は、"IndexName" を利用する


```typescript: QueryCommand
const queryRecords = async (userID: string, startDate: Date, endDate: Date) => {
    const paramsQuery = {
        TableName: "TestCompositeKey",
    //  IndexName: "index_post_id",
        ExpressionAttributeNames: {
            "#PK": "uid",
            "#SK": "timestamp#post_id"
        },
        ExpressionAttributeValues: {
            ":PK": userID,
            ":sortkeyval1": startDate.getTime(),
            ":sortkeyval2": endDate.getTime()
        },
        KeyConditionExpression: "#PK = :PK AND ( #SK  BETWEEN :sortkeyval1 AND :sortkeyval2 )"
    };
    console.log("query params:", inspect(paramsQuery));
    const queriedRecords = (await ddbDocClient.send(new QueryCommand(paramsQuery))).Items ?? [];
    return queriedRecords;
}
```
### 使える演算子と、関数はこちら
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html


# error 対処
## ValidationException: Invalid FilterExpression: Attribute name is a reserved keyword; reserved keyword: 
timestamp のような予約語使う場合は、プレースホルダーを使えってやつ

```typescript: "timestamp" が予約語なので、そのまま FilterExpression では使えない
    const params = {
        TableName: "TestCompositeKey",
        ExpressionAttributeValues: {
            // ":PK": userID,
            ":sortkeyval1": startDate.getTime(),
            ":sortkeyval2": endDate.getTime()
        },
        FilterExpression: "timestamp BETWEEN :sortkeyval1 AND :sortkeyval2"
    }
```

```typescript: 対策後。ExpressionAttributeNames で定義して使う
    const params = {
        TableName: "TestCompositeKey",
        ExpressionAttributeNames: {
            "#timestamp": "timestamp"
        },
        ExpressionAttributeValues: {
            // ":PK": userID,
            ":sortkeyval1": startDate.getTime(),
            ":sortkeyval2": endDate.getTime()
        },
        FilterExpression: "#timestamp BETWEEN :sortkeyval1 AND :sortkeyval2"
    }
    const usersGotten = (await ddbDocClient.send(new
```

## ValidationException: Query condition missed key schema element: uid
キーが無いよ。
そう、Key と KeyConditionExpression は同時には使えない

```typescript: エラー
  const paramsQuery = {
    TableName: "TestCompositeKey",
    Key: {  // これが効かない
      uid: userID
    },
    ExpressionAttributeNames: {
      "#SK": "timestamp#post_id"
    },
    ExpressionAttributeValues: {
      ":sortkeyval1": startDate.getTime(),
      ":sortkeyval2": endDate.getTime()
    },
    KeyConditionExpression: "#SK  BETWEEN :sortkeyval1 AND :sortkeyval2 "
  };
```

```typescript: 対策後: KeyConditionExpression でまとめればOK
  const paramsQuery = {
    TableName: "TestCompositeKey",
    ExpressionAttributeNames: {
      "#PK": "uid",
      "#SK": "timestamp#post_id"
    },
    ExpressionAttributeValues: {
      ":PK": userID,
      ":sortkeyval1": startDate.getTime(),
      ":sortkeyval2": endDate.getTime()
    },
    KeyConditionExpression: "#PK = :PK AND ( #SK  BETWEEN :sortkeyval1 AND :sortkeyval2 )"
  };
```

## ValidationException: One or more parameter values were invalid: Condition parameter type does not match schema type
Schema と違う型使ってるよって話

```typescript: エラー： timestamp は Epoch Number だけど、Sort Key にした際に、文字列になってる為
  const paramsQuery = {
    TableName: "TestCompositeKey",
    ExpressionAttributeNames: {
      "#PK": "uid",
      "#SK": "timestamp#post_id"
    },
    ExpressionAttributeValues: {
      ":PK": userID,
      ":sortkeyval1": startDate.getTime(),
      ":sortkeyval2": endDate.getTime()
    },
    KeyConditionExpression: "#PK = :PK AND ( #SK  BETWEEN :sortkeyval1 AND :sortkeyval2 )"
  };
```

```typescript: 対策後：文字列化するのみ
  const paramsQuery = {
    TableName: "TestCompositeKey",
    ExpressionAttributeNames: {
      "#PK": "uid",
      "#SK": "timestamp#post_id"
    },
    ExpressionAttributeValues: {
      ":PK": userID,
      ":sortkeyval1": `${startDate.getTime()}`,
      ":sortkeyval2": `${endDate.getTime()}`
    },
    KeyConditionExpression: "#PK = :PK AND ( #SK  BETWEEN :sortkeyval1 AND :sortkeyval2 )"
  };
```

## ValidationException: Value provided in ExpressionAttributeNames unused in expressions: keys: {#SK}

単に、未使用の ExpressionAttributeNames を残しておいちゃダメなので、消せってだけ

## ValidationException: Value provided in ExpressionAttributeValues unused in expressions: keys: {:sortkeyval2, :sortkeyval1}

同様に、ExpressionAttributeValues も使わないのは消しましょう

## ValidationException: Filter Expression can only contain non-primary key attributes: Primary key attribute: 

PK/SK を filterExpression に定義したらダメです

## ValidationException: KeyConditionExpressions must only contain one condition per key

PK/SK を、二回使ってはダメ
以下の場合、SK を二回使っているので error。対処法は BETWEEN を使えってこと

```typescript: エラー例
KeyConditionExpression: "#PK =:PK AND ( :sortkeyval1 <= #SK AND #SK < :sortkeyval2 )"
```

# VS Code 
## エラーメッセージなどは、CTRL + Click で該当コードへ
CTRL + Click で、Source<line/place> の該当箇所へ
※Enter が必要な場合も有り

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120072/e50b9b1e-4c50-bcdb-9e5d-f6cc60533333.png)


# template.yaml
## lambda console でデバッグする際は、Minify/Sourcemap を false にする
- Minify
    - コード見やすくしないと
- Sourcemap
    - Lambda 上の js コードを修正しながら行うのであれば、false のが分かりやすい
```yaml: 
    Metadata: # Manage esbuild properties
      BuildMethod: esbuild
      BuildProperties:
        Minify: false
        Target: "es2020"
        Sourcemap: false
```

# あとがき
PartiQL や SQL との比較もしてみたいなぁ