1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

DynamoDB: 渡された属性だけを更新する為に、動的に更新式を生成させる

Last updated at Posted at 2023-01-12

背景

DynamoDB を利用していて、データを更新しようとした際に、更新オブジェクトが動的な場合の対処方法の記録

GCP だと、Merge: true で終わっちゃうやつ?

GetCommand + Merge + PutCommand

この場合、lodash.merge で 取得したデータに、更新データを merge するだけで簡単に出来て、しかも PutCommand 時には、Key & Item で簡単に Update という名の置換が出来る

但し、以下課題がある

  1. 置換なので、たぶん Lambda で Insert Event が発生する
    • 本来なら更新なので、Modify になってくれないと困る
  2. Get & Put で二回 API Call される

更新式を動的に作成して UpdateCommand

これを実施する為に、以下を調査したので、いざ実践って話

結論

実装例

こんな感じで、Key (この場合は、uid)と更新データ(不定)を渡すと UpdateCommand で利用する UpdateCommandInput を返してくれるようにした

createUpdateCommandInput
const createUpdateCommandInput = (uid: string, userContent: any) => {
    // Key (PK/SK) の除外用。今回は、PK: uid として定義
    const isKey = (propertyName: string) => {
        return propertyName == "uid";
    }
    // 対象のプロパティ一覧の取得
    const targetProperties = Object.keys(userContent).filter((f) => !isKey(f));
    if (targetProperties.length == 0) {
        return "nothing is updated.";
    }
    // ベースとなる TableName/Key の定義。
    const keyCommandInput = {
        TableName: "user",
        Key: {
            uid: uid,
        },
    };
    // 動的に CommandInput を作る為の素地
    let setCommandInput = {
        UpdateExpression: "",
    };
    // 対象プロパティ一覧を元に、動的にオブジェクト生成
    targetProperties.forEach((f) => {
        // サブオブジェクトは置換。更新式対象にしたい場合は、(typeof postContent[k] == "object") で再帰処理を
        setCommandInput = _.merge(setCommandInput, {
            ExpressionAttributeNames: {
                [`#${f}`]: f
            },
            ExpressionAttributeValues: {
                [`:${f}`]: userContent[f]
            },
        });
    })
    // UpdateExpression は、文字列変形で突っ込むだけ
    setCommandInput.UpdateExpression = targetProperties.
        map((m) => `#${m}=:${m}`).
        reduce((previous, current, index) => `${index == 1 ? "SET " : ""}${previous}, ${current}`);
    // 出来たら、結合して終わり
    return _.merge(keyCommandInput, setCommandInput);
}

あとは、以下のようにして UpdateCommand に渡してやればOK

利用例
const resultUpdating = (await ddbDocClient.send(new UpdateCommand(updateCommandInput)));

動作例

利用データ
const updateContent = {
    uid: "uid012345",
    name: "test_user",
    age: 19,
};

更新対象無し

更新対象無し
console.log(createUpdateCommandInput(updateContent.uid, {}));
結果例
nothing is updated.

更新例1

更新例1
console.log(createUpdateCommandInput(updateContent.uid, updateContent));
結果例
{
  TableName: 'user',
  Key: { uid: 'uid012345' },
  UpdateExpression: 'SET #name=:name, #age=:age',
  ExpressionAttributeNames: { '#name': 'name', '#age': 'age' },
  ExpressionAttributeValues: { ':name': 'test_user', ':age': 19 }
}

更新例2

更新例2
console.log(createUpdateCommandInput(updateContent.uid, { ...updateContent, hoge: "hige"}));
結果例
{
  TableName: 'user',
  Key: { uid: 'uid012345' },
  UpdateExpression: 'SET #name=:name, #age=:age, #hoge=:hoge',
  ExpressionAttributeNames: { '#name': 'name', '#age': 'age', '#hoge': 'hoge' },
  ExpressionAttributeValues: { ':name': 'test_user', ':age': 19, ':hoge': 'hige' }
}

参考までの再帰版:未確認

const createUpdateCommandInput = (uid: string, userContent: any) => {
    // Key (PK/SK) の除外用。今回は、PK: uid として定義
    const isKey = (propertyName: string) => {
        return propertyName == "uid";
    }
    // 対象のプロパティ一覧の取得
    const targetProperties = Object.keys(userContent).filter((f) => !isKey(f));
    if (targetProperties.length == 0) {
        return "nothing is updated.";
    }
    // ベースとなる TableName/Key の定義。
    const keyCommandInput = {
        TableName: "user",
        Key: {
            uid: uid,
        },
    };
    
    // 動的に CommandInput を作る関数
    const createSetCommandInput = (object: any, prefix?: string) => {
        let setCommandInput = {
            UpdateExpression: "",
            ExpressionAttributeNames: {},
            ExpressionAttributeValues: {}
        };
        
        for (let key of Object.keys(object)) {
            let name = prefix ? `${prefix}.${key}` : key;
            let value = object[key];
            
            if (typeof value == "object") { // サブオブジェクトは再帰処理
                let subCommandInput = createSetCommandInput(value, name);
                setCommandInput.UpdateExpression += subCommandInput.UpdateExpression;
                setCommandInput.ExpressionAttributeNames = {...setCommandInput.ExpressionAttributeNames, ...subCommandInput.ExpressionAttributeNames};
                setCommandInput.ExpressionAttributeValues = {...setCommandInput.ExpressionAttributeValues, ...subCommandInput.ExpressionAttributeValues};
            } else { // サブオブジェクトでなければ更新式対象
                let placeholderName = `#${name.replace(/\./g,"_")}`;
                let placeholderValue = `:${name.replace(/\./g,"_")}`;
                
                setCommandInput.UpdateExpression += `${placeholderName}=${placeholderValue}, `;
                setCommandInput.ExpressionAttributeNames[placeholderName] = name;
                setCommandInput.ExpressionAttributeValues[placeholderValue] = value;
            }
        }
        
        return setCommandInput;
    };
    
    // 対象プロパティ一覧を元に、動的にオブジェクト生成
    let setCommandInput = createSetCommandInput(userContent);
    
     // UpdateExpression は、末尾のカンマを削除して SET を付ける
     setCommandInput.UpdateExpression =
      "SET " + setCommandInput.UpdateExpression.slice(0, -2);
    
     // 出来たら、結合して終わり
     return _.merge(keyCommandInput, setCommandInput);
}

あとがき

TypeScript の学習にはいい感じの課題だったわけだけど、こんな程度、AWS 側で用意してないのがなんか違和感がある。

出来る AWS Developper さまより「〇〇使うと一発ですよ」との突っ込みを期待しておわる :laughing:

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?