背景
DynamoDB を利用していて、データを更新しようとした際に、更新オブジェクトが動的な場合の対処方法の記録
GCP だと、Merge: true で終わっちゃうやつ?
案
GetCommand + Merge + PutCommand
この場合、lodash.merge で 取得したデータに、更新データを merge するだけで簡単に出来て、しかも PutCommand 時には、Key & Item で簡単に Update という名の置換が出来る
但し、以下課題がある
- 置換なので、たぶん Lambda で Insert Event が発生する
- 本来なら更新なので、Modify になってくれないと困る
- 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 さまより「〇〇使うと一発ですよ」との突っ込みを期待しておわる