[アップデート] Amazon DynamoDB で条件付き書き込みが失敗したアイテムをレスポンスに含めることが出来るようになりました(AWS SDK for JavaScript)

2023.07.09

こんにちは、CX事業本部 Delivery部の若槻です。

Amazon DynamoDB では、アイテム操作時に Condition expressions(条件式)を使用することにより、対象のアイテムのキーの存在や属性値に応じて、操作を実行するかどうかを制御することができます。これを条件付き書き込みと言います。

今回のアップデートにより、Amazon DynamoDB 条件付き書き込みが失敗したアイテムをレスポンスに含めることが出来るようになりました

これにより、今までは条件付き書き込みが失敗したアイテムをログ出力などしたい場合は別途読み取り操作を行う必要がありましたが、今後は条件付き書き込みのレスポンスに含めることができるようになりました。またレスポンスに含めた場合でも追加コストは発生しません。

リリース内容

本アップデートの機能は、AWS SDK for JavaScript v3 へは 2023/06/29 リリースの v3.363.0 で追加されました。

New Features

client-dynamodb: This release adds ReturnValuesOnConditionCheckFailure parameter to PutItem, UpdateItem, DeleteItem, ExecuteStatement, BatchExecuteStatement and ExecuteTransaction APIs. When set to ALL_OLD, API returns a copy of the item as it was when a conditional write failed (cef0845a)

Pull Request の変更内容を見ると、ReturnValuesOnConditionCheckFailure パラメーターで ALL_OLD を指定にすれば、条件付き書き込みが失敗したアイテムをレスポンスに含めることができるようになっています。

node_modules/@aws-sdk/client-dynamodb/dist-types/models/models_0.d.ts

/**
 * @public
 * @enum
 */
export const ReturnValuesOnConditionCheckFailure = {
  ALL_OLD: "ALL_OLD",
  NONE: "NONE",
} as const;

試してみた

事前準備

事前準備として、AWS CDK で検証用のテーブルを作成しておきます。

lib/cdk-sample-stack.ts

import { aws_dynamodb, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkSampleStack extends Stack {
  public readonly myFileObjectKey: string;

  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    new aws_dynamodb.Table(this, 'myTable', {
      tableName: 'myTable',
      partitionKey: { name: 'id', type: aws_dynamodb.AttributeType.STRING },
      billingMode: aws_dynamodb.BillingMode.PAY_PER_REQUEST,
    });
  }
}

動作確認

AWS SDK for JavaScript v3 で PutItemCommand を使って試してみます。

レスポンスに含める場合

ReturnValuesOnConditionCheckFailure パラメーターで ALL_OLD を指定し、条件付き書き込み失敗時にレスポンスに含めるようにしてみます。

script.ts

import {
  DynamoDBClient,
  PutItemCommand,
  ReturnValuesOnConditionCheckFailure,
} from '@aws-sdk/client-dynamodb';

const client = new DynamoDBClient({ region: 'ap-northeast-1' });

const params = {
  TableName: 'myTable',
  Item: {
    id: { S: 'myId_1' },
    name: { S: 'myName_1' },
  },
  ConditionExpression: 'attribute_not_exists(id)', // 書き込み条件:アイテムが存在していないこと
  ReturnValuesOnConditionCheckFailure:
    ReturnValuesOnConditionCheckFailure.ALL_OLD, // 条件付き書き込み失敗時にレスポンスに含める
};

const command = new PutItemCommand(params);

const run = async (): Promise<void> => {
  try {
    const response = await client.send(command);
    console.log(response);
  } catch (err) {
    const error = err as Error;
    if (error.name === 'ConditionalCheckFailedException') {
      console.log('条件付き書き込み失敗');
    }
    console.error(error);
  }
};

run();

初回の PutItemCommand は成功します。

$ npx ts-node script.ts
{
  '$metadata': {
    httpStatusCode: 200,
    requestId: 'AN6J4G1DTS1G538GDDAG7M1T17VV4KQNSO5AEMVJF66Q9ASUAAJG',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  }
}

2回目の PutItemCommand は条件付き書き込みが失敗しますが、その際にレスポンスの Item には書き込み対象のアイテムが含まれています。

$ npx ts-node script.ts
条件付き書き込み失敗
ConditionalCheckFailedException: The conditional request failed
    at de_ConditionalCheckFailedExceptionRes (/Users/wakatsuki.ryuta/projects/cm-rwakatsuki/cdk_sample_app/node_modules/@aws-sdk/client-dynamodb/dist-cjs/protocols/Aws_json1_0.js:2671:23)
    at de_PutItemCommandError (/Users/wakatsuki.ryuta/projects/cm-rwakatsuki/cdk_sample_app/node_modules/@aws-sdk/client-dynamodb/dist-cjs/protocols/Aws_json1_0.js:1875:25)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async /Users/wakatsuki.ryuta/projects/cm-rwakatsuki/cdk_sample_app/node_modules/@smithy/middleware-serde/dist-cjs/deserializerMiddleware.js:7:24
    at async /Users/wakatsuki.ryuta/projects/cm-rwakatsuki/cdk_sample_app/node_modules/@aws-sdk/middleware-signing/dist-cjs/awsAuthMiddleware.js:14:20
    at async /Users/wakatsuki.ryuta/projects/cm-rwakatsuki/cdk_sample_app/node_modules/@smithy/middleware-retry/dist-cjs/retryMiddleware.js:27:46
    at async /Users/wakatsuki.ryuta/projects/cm-rwakatsuki/cdk_sample_app/node_modules/@aws-sdk/middleware-logger/dist-cjs/loggerMiddleware.js:7:26
    at async run (/Users/wakatsuki.ryuta/projects/cm-rwakatsuki/cdk_sample_app/script.ts:24:22) {
  '$fault': 'client',
  '$metadata': {
    httpStatusCode: 400,
    requestId: '51AT825HPGHE08QQEU190P3BQNVV4KQNSO5AEMVJF66Q9ASUAAJG',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  },
  Item: { id: { S: 'myId_1' }, name: { S: 'myName_1' } },
  __type: 'com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException'
}

レスポンスに含めない場合

比較のために、ReturnValuesOnConditionCheckFailure パラメーターで NONE を指定し、条件付き書き込み失敗時にレスポンスに含めない場合も試してみます。

script.ts

const params = {
  TableName: 'myTable',
  Item: {
    id: { S: 'myId_2' },
    name: { S: 'myName_2' },
  },
  ConditionExpression: 'attribute_not_exists(id)',
  ReturnValuesOnConditionCheckFailure: ReturnValuesOnConditionCheckFailure.NONE,
};

初回の PutItemCommand は成功します。

$ npx ts-node script.ts
{
  '$metadata': {
    httpStatusCode: 200,
    requestId: 'UT6IN7PK6D6Q8OUAE200B50KVFVV4KQNSO5AEMVJF66Q9ASUAAJG',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  }
}

2回目の PutItemCommand は条件付き書き込みが失敗しますが、その際にレスポンスの Item には書き込み対象のアイテムが含まれていません。

$ npx ts-node script.ts
条件付き書き込み失敗
ConditionalCheckFailedException: The conditional request failed
    at de_ConditionalCheckFailedExceptionRes (/Users/wakatsuki.ryuta/projects/cm-rwakatsuki/cdk_sample_app/node_modules/@aws-sdk/client-dynamodb/dist-cjs/protocols/Aws_json1_0.js:2671:23)
    at de_PutItemCommandError (/Users/wakatsuki.ryuta/projects/cm-rwakatsuki/cdk_sample_app/node_modules/@aws-sdk/client-dynamodb/dist-cjs/protocols/Aws_json1_0.js:1875:25)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async /Users/wakatsuki.ryuta/projects/cm-rwakatsuki/cdk_sample_app/node_modules/@smithy/middleware-serde/dist-cjs/deserializerMiddleware.js:7:24
    at async /Users/wakatsuki.ryuta/projects/cm-rwakatsuki/cdk_sample_app/node_modules/@aws-sdk/middleware-signing/dist-cjs/awsAuthMiddleware.js:14:20
    at async /Users/wakatsuki.ryuta/projects/cm-rwakatsuki/cdk_sample_app/node_modules/@smithy/middleware-retry/dist-cjs/retryMiddleware.js:27:46
    at async /Users/wakatsuki.ryuta/projects/cm-rwakatsuki/cdk_sample_app/node_modules/@aws-sdk/middleware-logger/dist-cjs/loggerMiddleware.js:7:26
    at async run (/Users/wakatsuki.ryuta/projects/cm-rwakatsuki/cdk_sample_app/script.ts:23:22) {
  '$fault': 'client',
  '$metadata': {
    httpStatusCode: 400,
    requestId: '2O6RTQCJKPEJPV91P98K73D1FVVV4KQNSO5AEMVJF66Q9ASUAAJG',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  },
  Item: undefined,
  __type: 'com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException'
}

AWS SDK for Python の場合

AWS SDK for Python で試してみた記事は下記になります。

おわりに

Amazon DynamoDB で条件付き書き込みが失敗したアイテムをレスポンスに含めることが出来るようになったので、AWS SDK for JavaScript v3 で試してみました。

条件付き書き込みを行う際のエラーハンドリングが容易になるので是非とも活用したいですね。

参考

以上