DynamoDB: 属性が動的なアイテムでの、既存アイテムの更新方法の検討

# 背景
DynamoDB のデータ更新時に、データのマージをしたかったので、方法を調査してみた

# 結論
とりあえず、以下2つの案が出来たけど・・なんだかなぁ‥という状況。
なんかもっと良い方法がありそうなんだが・・

1. [Update したい 属性に応じて、更新式を構成する](#updatecommandinput-をproperty-により-onoff-切り替える)
    - 更新式の構成自体が面倒
1. [現状item を Get で取得して、マージ後に、Put で上書き](#getcommand-で既存データ取得してmerge-してから-put)
    - Get でデータ取得が先に必要で、その分の処理が無駄・・。

# 試行
## PutCommand では、上書きされるので、使えない
Document にあるように・・"completely replace" されます。

試しにやってみたけど、完全に新しくなりました。

> Creates a new item, or replaces an old item with a new item. **If an item that has the same primary key as the new item already exists in the specified table, the new item completely replaces the existing item.** You can perform a conditional put operation (add a new item if one with the specified primary key doesn't exist), or replace an existing item if it has certain attribute values. You can return the item's attribute values in the same operation, using the ReturnValues parameter.

https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-dynamodb/classes/putitemcommand.html

## UpdateCommand でなら出来そう
### UpdateCommandInput に、undefined を渡すとエラー
> error: ValidationException: ExpressionAttributeValues must not be empty

まぁ、そうですよね。

### UpdateCommandInput 設定時に　Undefined の場合に、空文字にすると、\<empty> 設定されるのでダメ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120072/f1ea1ff2-8bee-70b4-6b17-d9b78c98fa81.png)

```typescript: 
ExpressionAttributeValues: {
    ":data": content.data ?? "",
},
```

更新式にて、SET に定義があるから、ダメなんだろうなぁ・・と

### Client 生成時のオプションで、marshalOption を true にすると、null 設定されるのでダメ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120072/7dff9896-0c58-4c58-8f2d-4d8fda0878dc.png)


まぁ、empty を単に、null にしてくれるだけなので、対象の値が null　になるだけだった

```typescript: marshalOption
const marshallOptions = {
    // Whether to automatically convert empty strings, blobs, and sets to `null`.
    convertEmptyValues: true, // false, by default.
    // Whether to remove undefined values while marshalling.
    removeUndefinedValues: true, // false, by default.
    // Whether to convert typeof object to map attribute.
    convertClassInstanceToMap: true, // false, by default.
};
```

### UpdateCommandInput を、Property により On/Off 切り替える・・
幸い、オブジェクト結合方法を調べたばかりだったけど・・

https://qiita.com/baku2san/items/916036cc6ce82055c706

ん－・・無様

以下は、data2 という属性が定義されてくるかどうかにより、更新式自体を変更するようにしてみた例だが、
大量の属性がある場合に、こんなの定義しまくるなんてあり得ない・・

これを動的に生成する？？

```typescript: data2 プロパティの切り替え例
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));
}
```

### GetCommand で既存データ取得して、Merge してから Put

PutCommand なら、Expression を頑張って定義する必要もないので、取得したデータをマージしたオブジェクトをそのまま Put してやれば簡単に出来る。
ただ・・ GetCommand の発行が必要なので、無駄な処理があるといえばある・・

```typescript: Get & Put での Update 例
import _ from "lodash";

const ddbDocClient = DynamoDBDocumentClient.from(new DynamoDBClient(clientConfig)); //, translateConfig);
const getItem = async (content: any) => {
    const params = {
        TableName: "TestCompositeKey",
        Key: {
            uid: content.uid,
            "timestamp#post_id": content["timestamp#post_id"],
        },
    };
    return await (await ddbDocClient.send(new GetCommand(params))).Item;
};
const putItem = async (content: any) => {
    const params = {
        TableName: "TestCompositeKey",
        Item: content,
    };
    return await ddbDocClient.send(new PutCommand(params));
};
const targetitem = {
    uid: "user02",
    "timestamp#post_id": "1670853386397#1",
};
const currentItem = await getItem(targetitem);
const updatingItem = {
    data3: "value3",
    data4: 4,
}
const mergedItem = _.merge(currentItem, updatingItem);
const result4 = await putItem(mergedItem);
```

# あとがき
TypeScript を学習していけば、プロパティから更新式を自動生成する方法でもっと良い方法が見つかるかもなので、
その辺りを少し調べたいところ。