[Amazon DynamoDB] 既存の属性値を利用したマップを項目に追加する(AWS SDK for JavaScript v3)

2023.05.20

こんにちは、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 パラメーターに記述しています。

  • マップ attrsattr1 属性に、既存の attr 属性の値を設定
  • マップ attrsattr2 属性に、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 を作成
  • マップ attrsattr1 属性に、既存の 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回目
    • マップ attrsattr1 属性に、既存の attr 属性の値を設定
    • マップ attrsattr2 属性に、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回のコマンド実行で出来そうで出来なかったのがもどかしかったです。

以上