DynamoDB Transactionsで複数テーブルにまたがる書き込み処理をしてみる

2022.04.15

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

広島の吉川です。

DynamoDB Transactionsが気になっていながらあまり触ったことがなかったため、簡単にではありますが検証してみました。

Amazon DynamoDB Transactions: 仕組み - Amazon DynamoDB

TransactWriteItems オペレーションは、含まれるすべてのアクションを正常に完了する必要があり、そうでない場合は変更がまったく行われないという点で BatchWriteItem オペレーションとは異なります。

ということなので、

  • トランザクション内で2つの書き込み操作を行い、両方成功する場合→すべての書き込み処理が有効になる
  • トランザクション内で2つの書き込み操作を行い、後者のみ失敗した場合→失敗していない前者を含むすべての書き込み処理が無効になる

のパターンを実際に手を動かしながら試してみたいと思います。また、単一テーブルだけでなく複数テーブルにまたぐ処理も試してみます。

環境

  • node 16.14.2
  • npm 8.6.0
  • typescript 4.6.3
  • @faker-js/faker 6.1.2
  • @aws-sdk/* 3.67.0

DynamoDBテーブルを準備

MyUsersテーブル

AWSマネジメントコンソールより、DynamoDBコンソールを開き

  • テーブル名: "MyUsers"
  • パーティションキー: "id" / 文字列
  • 設定: 設定をカスタマイズ
  • 読み込み/書き込みキャパシティーの設定: オンデマンド

の設定でテーブルを作成します。

MyArticlesテーブル

同じように

  • テーブル名: "MyArticles"
  • パーティションキー: "id" / 文字列
  • 設定: 設定をカスタマイズ
  • 読み込み/書き込みキャパシティーの設定: オンデマンド

の設定でテーブルを作成します。

単一テーブルでトランザクション書き込み

成功する場合

まずは単一テーブルでトランザクション書き込みしてみます。

import { DynamoDB } from '@aws-sdk/client-dynamodb'
import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb'
import { faker } from '@faker-js/faker'

const ddb = new DynamoDB({})
const ddbDoc = DynamoDBDocument.from(ddb)

;(async () => {
  await ddbDoc.transactWrite({
    TransactItems: [
      {
        Put: {
          TableName: 'MyUsers',
          Item: {
            id: '1',
            name: faker.name.findName(),
            address: faker.address.city(),
          },
        },
      },
      {
        Put: {
          TableName: 'MyUsers',
          Item: {
            id: '2',
            name: faker.name.findName(),
            address: faker.address.city(),
          },
        },
      },
    ],
  })
})()

MyUsersテーブルに対し2件のItemを書き込みます。

実行後、テーブルをScanして中身を確認します。

aws dynamodb scan --table-name MyUsers
{
    "Items": [
        {
            "address": {
                "S": "North Verlabury"
            },
            "id": {
                "S": "2"
            },
            "name": {
                "S": "Clarence Casper PhD"
            }
        },
        {
            "address": {
                "S": "Hilpertstad"
            },
            "id": {
                "S": "1"
            },
            "name": {
                "S": "Dallas Dickens"
            }
        }
    ],
    "Count": 2,
    "ScannedCount": 2,
    "ConsumedCapacity": null
}

2件Itemが入っていますね。意図通りの結果です。

失敗させてみる

では、わざと2件目のItem作成を失敗させてみます。

コードから以下の行を削除します。

      {
        Put: {
          TableName: 'MyUsers',
          Item: {
-           id: '2',
            name: faker.name.findName(),
            address: faker.address.city(),
          },
        },
      },

作成しようとしているItemにパーティションキー id がないのでエラーになるはずです。DynamoDBテーブルを一度クリア (マネジメントコンソールから手動でItemを削除) し、編集後のコードを再実行してみます。

すると、

TransactionCanceledException: Transaction cancelled, please refer cancellation reasons for specific reasons [None, ValidationError]

このように TransactionCanceledException というエラーが発生します。

先程と同じようにテーブルの中身を確認します。

aws dynamodb scan --table-name MyUsers
{
    "Items": [],
    "Count": 0,
    "ScannedCount": 0,
    "ConsumedCapacity": null
}

今度は何もItemが追加されていない結果となりました。2件目の操作に失敗したので、1件目の書き込みも無効となったことがわかります。

複数テーブルにまたがるトランザクション書き込み

成功する場合

続いて、複数テーブルにまたがる書き込み処理で試してみます。

import { DynamoDB } from '@aws-sdk/client-dynamodb'
import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb'
import { faker } from '@faker-js/faker'

const ddb = new DynamoDB({})
const ddbDoc = DynamoDBDocument.from(ddb)

;(async () => {
  await ddbDoc.transactWrite({
    TransactItems: [
      {
        Put: {
          TableName: 'MyUsers',
          Item: {
            id: '1',
            name: faker.name.findName(),
            address: faker.address.city(),
          },
        },
      },
      {
        Put: {
          TableName: 'MyArticles',
          Item: {
            id: '1',
            title: faker.lorem.sentence(),
            body: faker.lorem.sentences(),
          },
        },
      },
    ],
  })
})()

1件目をMyUsersテーブル、2件目をMyArticlesテーブルに書き込むコードです。

実行後、それぞれのテーブルをScanして確認します。

aws dynamodb scan --table-name MyUsers
{
    "Items": [
        {
            "address": {
                "S": "St. Clair Shores"
            },
            "id": {
                "S": "1"
            },
            "name": {
                "S": "Irving Morar"
            }
        }
    ],
    "Count": 1,
    "ScannedCount": 1,
    "ConsumedCapacity": null
}
aws dynamodb scan --table-name MyArticles
{
    "Items": [
        {
            "id": {
                "S": "1"
            },
            "title": {
                "S": "Accusantium aut mollitia omnis."
            },
            "body": {
                "S": "Eligendi maiores qui qui esse tenetur consequuntur. Ratione beatae sint nobis labore ut ipsa delectus non laudantium. Placeat ullam porro quod. Quae tempora harum. Eos mollitia ea quo voluptatem consequuntur facere. Dolorem omnis iusto vero voluptatem et excepturi ut non voluptas."
            }
        }
    ],
    "Count": 1,
    "ScannedCount": 1,
    "ConsumedCapacity": null
}

意図通り1件ずつItemが入っています。

失敗させてみる

では、こちらも2件目の書き込みで失敗させてみます。

      {
        Put: {
          TableName: 'MyArticles',
          Item: {
-           id: '1',
            title: faker.lorem.sentence(),
            body: faker.lorem.sentences(),
          },
        },
      },

MyArticlesへの書き込みが失敗するようにパーティションキー id を削除しました。

各テーブルをクリアしてから実行すると、

TransactionCanceledException: Transaction cancelled, please refer cancellation reasons for specific reasons [None, ValidationError]

今度も TransactionCanceledException エラーが発生しました。

各テーブルを確認します。

aws dynamodb scan --table-name MyUsers
{
    "Items": [],
    "Count": 0,
    "ScannedCount": 0,
    "ConsumedCapacity": null
}
aws dynamodb scan --table-name MyArticles
{
    "Items": [],
    "Count": 0,
    "ScannedCount": 0,
    "ConsumedCapacity": null
}

どちらのテーブルも空です。複数テーブルにまたがった場合も、トランザクション内のある操作が失敗すればそれ以外の操作も含めて無効にしてくれることがわかります。

まとめ

複数処理のみならず複数テーブルにまたがってトランザクション処理ができるのはかなり便利な気がします。複数書き込み失敗時のロールバック処理を自分で書いていくのはなかなか大変なので、使える局面では積極的に活用すると良いのではないかと思いました。

参考