こんにちは、CX事業本部 Delivery部の若槻です。
Amazon DynamoDB テーブルに格納されている下記の形式の項目があります。
更新前
{
"id": "d001",
"attr": 1234567890
}
この項目を下記の形式に更新したいです。attrs
というマップ属性を設けて、attr1
の値は attr
をそのまま設定し、attr2
の値は 0
を設定します。
更新後
{
"id": "d001",
"attrs": {
"attr1": 1234567890,
"attr2": 0,
}
}
今回は、この更新を AWS SDK for JavaScript v3 で行う方法を確認してみました。
結論
結論から言いますと、下記のように UpdateItem
コマンドを2回に分けて実行する必要がありました。
script.ts
import { DynamoDBClient, UpdateItemCommand } from '@aws-sdk/client-dynamodb';
import { marshall } from '@aws-sdk/util-dynamodb';
const REGION = 'ap-northeast-1';
const TableName = 'my_table';
const client = new DynamoDBClient({ region: REGION });
// attrs 属性をマップとして作成
const params1 = {
TableName,
Key: marshall({
id: 'd001',
}),
UpdateExpression: 'SET #attrs = :attrs',
ExpressionAttributeNames: {
'#attrs': 'attrs',
},
ExpressionAttributeValues: marshall({
':attrs': {},
}),
};
// attr1 と attr2 を設定し、attr を削除
const params2 = {
TableName,
Key: marshall({
id: 'd001',
}),
UpdateExpression:
'SET #attrs.#attr1 = #attr, #attrs.#attr2 = :zero REMOVE #attr',
ExpressionAttributeNames: {
'#attr': 'attr',
'#attrs': 'attrs',
'#attr1': 'attr1',
'#attr2': 'attr2',
},
ExpressionAttributeValues: marshall({
':zero': 0,
}),
};
const run = async () => {
try {
const data1 = await client.send(new UpdateItemCommand(params1));
console.log(data1);
const data2 = await client.send(new UpdateItemCommand(params2));
console.log(data2);
} catch (err) {
console.error(err);
}
};
run();
試行錯誤ログ
検証用テーブル作成
AWS CDK(TypeScript)で検証用の DynamoDB テーブルを作成します。
lib/cdk-sample-app.ts
import { aws_dynamodb, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class CdkSampleStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps) {
super(scope, id, props);
new aws_dynamodb.Table(this, 'my_table', {
tableName: 'my_table',
partitionKey: { name: 'id', type: aws_dynamodb.AttributeType.STRING },
billingMode: aws_dynamodb.BillingMode.PAY_PER_REQUEST,
});
}
}
試行その1
まず、次の処理を1回のコマンドで行う方法ことを考えました。これらの処理はすべて UpdateExpression
パラメーターに記述しています。
- マップ
attrs
のattr1
属性に、既存のattr
属性の値を設定 - マップ
attrs
のattr2
属性に、0
を設定 attr
属性を削除
script.ts
import { DynamoDBClient, UpdateItemCommand } from '@aws-sdk/client-dynamodb';
import { marshall } from '@aws-sdk/util-dynamodb';
const REGION = 'ap-northeast-1';
const TableName = 'my_table';
const client = new DynamoDBClient({ region: REGION });
const params = {
TableName,
Key: marshall({
id: 'd001',
}),
UpdateExpression:
'SET #attrs.#attr1 = #attr, #attrs.#attr2 = :zero REMOVE #attr',
ExpressionAttributeNames: {
'#attr': 'attr',
'#attrs': 'attrs',
'#attr1': 'attr1',
'#attr2': 'attr2',
},
ExpressionAttributeValues: marshall({
':zero': 0,
}),
};
const run = async () => {
try {
const data = await client.send(new UpdateItemCommand(params));
console.log(data);
} catch (err) {
console.error(err);
}
};
run();
しかし実行すると、下記のようなエラーが発生しました。
$ npx ts-node script.ts
ValidationException: The document path provided in the update expression is invalid for update
原因は、項目にマップ attrs
属性が存在しないためでした。マップ属性の子要素を設定するには、そのマップ属性が予め存在することが必要なようです。
試行その2
次に ExpressionAttributeValues
パラメーターで子要素を設定済みのマップ属性 attrs
を定義して作成しようと考えました。
script.ts
const params = {
TableName,
Key: marshall({
id: 'd001',
}),
UpdateExpression: 'SET #attrs = :attrs REMOVE #attr',
ExpressionAttributeNames: {
'#attr': 'attr',
'#attrs': 'attrs',
},
ExpressionAttributeValues: marshall({
':attrs': {
attr1: '#attr',
attr2: 0,
},
}),
};
するとコマンド実行は成功しましたが、attr1
の値が期待通りに設定されていません。
result
{
"id": "d001",
"attrs": {
"attr1": "#attr",
"attr2": 0
}
}
ExpressionAttributeNames
パラメーターで定義した #attr
が展開されないままとなっていますね。Expression パラメーター内でのみ使えるようです。
試行その3
次に、ここまでの試行を踏まえて下記の処理を1回のコマンドで行う方法を考えてみました。
attr2
属性に、0
を設定したマップattrs
を作成- マップ
attrs
のattr1
属性に、既存のattr
属性の値を設定 attr
属性を削除
script.ts
const params = {
TableName,
Key: marshall({
id: 'd001',
}),
UpdateExpression: 'SET #attrs = :attrs, #attrs.#attr1 = #attr REMOVE #attr',
ExpressionAttributeNames: {
'#attr': 'attr',
'#attrs': 'attrs',
'#attr1': 'attr1',
},
ExpressionAttributeValues: marshall({
':attrs': {
attr2: 0,
},
}),
};
しかし実行すると、下記のようなエラーが発生しました。
$ npx ts-node script.ts
ValidationException: Invalid UpdateExpression: Two document paths overlap with each other; must remove or rewrite one of these paths; path one: [attrs], path two: [attrs, attr1]
UpdateExpression
パラメーターの中で attrs
属性の更新を2回行おうとしているため、エラーが発生しています。 Expression パラメーターの中では同じ属性は2回以上更新できないようです。
改めて結論
ここまでの試行錯誤の結果、一回の UpdateItem
コマンドの実行では期待した更新処理ができないことが分かりました。そこで、下記のように UpdateItem
コマンドを2回に分けて実行することで実現できました。
- 1回目
- 空のマップ
attrs
属性を作成
- 空のマップ
- 2回目
- マップ
attrs
のattr1
属性に、既存のattr
属性の値を設定 - マップ
attrs
のattr2
属性に、0
を設定 attr
属性を削除
- マップ
script.ts
import { DynamoDBClient, UpdateItemCommand } from '@aws-sdk/client-dynamodb';
import { marshall } from '@aws-sdk/util-dynamodb';
const REGION = 'ap-northeast-1';
const TableName = 'my_table';
const client = new DynamoDBClient({ region: REGION });
// attrs 属性をマップとして作成
const params1 = {
TableName,
Key: marshall({
id: 'd001',
}),
UpdateExpression: 'SET #attrs = :attrs',
ExpressionAttributeNames: {
'#attrs': 'attrs',
},
ExpressionAttributeValues: marshall({
':attrs': {},
}),
};
// attr1 と attr2 を設定し、attr を削除
const params2 = {
TableName,
Key: marshall({
id: 'd001',
}),
UpdateExpression:
'SET #attrs.#attr1 = #attr, #attrs.#attr2 = :zero REMOVE #attr',
ExpressionAttributeNames: {
'#attr': 'attr',
'#attrs': 'attrs',
'#attr1': 'attr1',
'#attr2': 'attr2',
},
ExpressionAttributeValues: marshall({
':zero': 0,
}),
};
const run = async () => {
try {
const data1 = await client.send(new UpdateItemCommand(params1));
console.log(data1);
const data2 = await client.send(new UpdateItemCommand(params2));
console.log(data2);
} catch (err) {
console.error(err);
}
};
run();
必要に応じてマップ属性の有無を確認する
今回はマップ attr
属性が存在しない前提で実装しましたが、実際にはマップ属性が存在する場合もあると思います。その場合は、マップ属性の有無を確認するようにしましょう。
確認方法は下記の記事が、SDK for Java を利用した内容ではありますが、参考になると思います。
おわりに
Amazon DynamoDB で既存の属性値を利用したマップを項目に追加する方法を確認してみました。
1回のコマンド実行で出来そうで出来なかったのがもどかしかったです。
以上