CLOVER🍀

That was when it all began.

Amazon DynamoDBロヌカル版DynamoDB Local ずDocumentClientで、トランザクションを詊す

これは、なにをしたくお曞いたもの

今回は、Amazon DynamoDBのトランザクションを詊しおみようかなず思いたす。

DynamoDB トランザクションで複雑なワークフローを管理する - Amazon DynamoDB

Amazon DynamoDBのトランザクション

Amazon DynamoDBにはトランザクションがあるようです。

DynamoDB トランザクションで複雑なワークフローを管理する - Amazon DynamoDB

ACIDが実珟される、ず蚀っおいたす。

トランザクションによっお DynamoDB に䞍可分性、䞀貫性、分離性、耐久性 (ACID) が実珟されるため、アプリケヌション内でのデヌタの粟床を維持するこずができたす。

ずはいっおも、RDBMSでいうトランザクションずは差はあるでしょう。

トランザクションにはTransactWriteItemsずTransactGetItemsの2皮類の操䜜があり、曞き蟌みおよび読み蟌みでグルヌプ化
できるようです。

トランザクション曞き蟌み API を䜿甚しお、耇数の Put、Update、Delete、ConditionCheck の各アクションをグルヌプ化できたす。その埌、アクションを単䞀の TransactWriteItems オペレヌションずしお送信できたす。このオペレヌションはナニットずしお成功たたは倱敗したす。同じこずが耇数の Get アクションにも圓おはたりたす。この堎合、1 ぀の TransactGetItems オペレヌションずしおグルヌプ化し、送信できたす。

もっず詳しい内容は、こちらのペヌゞに蚘茉がありたす。

Amazon DynamoDB Transactions: 仕組み - Amazon DynamoDB

たずは、TransactWriteItems APIから。

Amazon DynamoDB Transactions: 仕組み / TransactWriteItems API

TransactWriteItemsは、最倧25個の曞き蟌み操䜜をたずめお行うAPIのようです。

TransactWriteItems は、最倧 25 の曞き蟌みアクションを 1 ぀のオヌルオアナッシングオペレヌションにグルヌプ化する、同期的でべき等な曞き蟌みオペレヌションです。これらのアクションは、同じ AWS アカりントおよび同じリヌゞョン内の 1 ぀以䞊の DynamoDB テヌブルにある最倧 25 個の異なる項目をタヌゲットできたす。トランザクション内のアむテムの合蚈サむズは 4 MB を超えるこずはできたせん。すべお成功するかどれも成功しないかのどちらずなるように、アトミックに実行されたす。

同䞀AWSアカりント、同䞀リヌゞョンの制限、たずめお実行した時のデヌタ量にも制限があるようです。

こう曞くずバッチ凊理のようなむメヌゞを受けたすが、凊理がアトミックに行われるこずがバッチ凊理ずの差異のようです。

TransactWriteItems オペレヌションは、含たれるすべおのアクションを正垞に完了する必芁があり、そうでない堎合は倉曎がたったく行われないずいう点で BatchWriteItem オペレヌションずは異なりたす。BatchWriteItem オペレヌションでは、バッチ内の䞀郚のアクションのみ成功し、他のアクションは成功しないこずがあり埗たす。

そもそも、バッチ凊理甚のAPIは別にありたしたね 。

TransactWriteItemsトランザクションに含める操䜜の䞭に、同じアむテムを凊理する操䜜を耇数含むこずはできたせん。

同じトランザクション内の耇数のオペレヌションが同じ項目をタヌゲットずするこずはできたせん。たずえば、同じトランザクション内で同じ項目に察しお ConditionCheck を実行し、Update アクションも実行するこずはできたせん。

TransactWriteItemsに含めるこずができる操䜜は、以䞋の4぀です。

  • Put
  • Update
  • Delete
  • ConditionCheck

ConditionCheckずいう操䜜が芋慣れなかったのですが、これはTransactWriteItemsで䜿える操䜜のようですね。

TransactWriteItemsに぀いおは、「クラむアントトヌクン」ずいうものをリク゚ストに含めるこずで、べき等であるこずを
確認できたす。

TransactWriteItems 呌び出しを行っおリク゚ストがべき等であるこずを確認するずき、オプションでクラむアントトヌクンを含めるこずができたす。トランザクションをべき等にするず、接続のタむムアりトや他の接続の問題のために同じオペレヌションが耇数回送信された堎合に、アプリケヌション゚ラヌを防ぐこずができたす。

元の TransactWriteItems 呌び出しが成功した堎合、同じクラむアントトヌクンを䜿甚したその埌の TransactWriteItems 呌び出しが倉曎を加えずに正垞に結果を返したす。

クラむアントトヌクンは䜿甚したリク゚ストから10分間有効で、10分経過するず新しいリク゚ストずしお扱われたす。
たた、10分以内に同じクラむアントトヌクンを䜿っおリク゚ストを繰り返す堎合に、含たれるリク゚ストの内容を倉曎するず
Amazon DynamoDBよりIdempotentParameterMismatch゚ラヌがスロヌされたす。

TransactWriteItemsが倱敗する条件はいく぀かあるようですが、以䞋あたりは泚意でしょうか。

  • あるTransactWriteItemsリク゚スト内の1぀以䞊のアむテムに察する凊理が、他に継続䞭のTransactWriteItemsオペレヌションず競合する
    • この堎合、TransactionCanceledException゚ラヌがスロヌされる
  • トランザクションを完了するプロビゞョンドキャパシティヌが足りない
  • 項目サむズが倧きくなりすぎる (400 KB 超)、ロヌカルセカンダリむンデックス (LSI) が倧きくなりすぎる、たたはトランザクションにより倉曎が加えられたためにこの条件を満たしおしたう堎合

トランザクションの競合に぀いおは、詳しくは以䞋に曞いおありたす。

Amazon DynamoDB Transactions: 仕組み / DynamoDB でのトランザクション競合の凊理

PutItemやUpdateItem、DeleteItemがトランザクションず競合しお拒吊された堎合は、TransactionConflictExceptionが
スロヌされるようです。

次は、TransactGetItems APIに぀いお。

Amazon DynamoDB Transactions: 仕組み / TransactGetItems API

TransactGetItemsは、TransactWriteItemsの読み蟌み操䜜版ずいう感じですね。

TransactGetItems は、最倧 25 個の Get アクションをたずめおグルヌプ化する同期読み取りオペレヌションです。これらのアクションは、同じ AWS アカりントおよびリヌゞョン内の 1 ぀以䞊の DynamoDB テヌブルにある最倧 25 個の異なる項目をタヌゲットにするこずができたす。トランザクション内の項目の合蚈サむズは 4 MB を超えるこずはできたせん。

Amazon DynamoDBはトランザクションずしお、ReadずWriteは完党に分離されるようですね。

たあ、RDBMSのトランザクションのむメヌゞよりも、バッチ凊理に近い印象をやはり持っおしたうのですが。

TransactGetItemsに含めるこずができる操䜜は、Getのみです。

TransactGetItemsは、操䜜察象のアむテムに察する凊理が実行䞭のTransactWriteItemsに含たれる操䜜ず競合する堎合、
TransactionCanceledExceptionで倱敗するようです。

TransactGetItems リク゚ストが、TransactWriteItems リク゚スト内の 1 ぀以䞊の項目に察する継続䞭の TransactGetItems オペレヌションず競合する堎合。この堎合、リク゚ストは TransactionCanceledException で倱敗したす。

トランザクションの分離レベル。

Amazon DynamoDB Transactions: 仕組み / DynamoDB トランザクションの分離レベル

TransactWriteItemsおよびTransactGetItemsのトランザクション分離レベルは、Serializableのようです。

なので、トランザクションは耇数同時には実行できないこずになりたす。

盎列化可胜分離レベルでは、耇数の同時オペレヌションの結果は、前のオペレヌションが完了するたでオペレヌションが開始されない堎合ず同じになりたす。

ロック察象は明蚘されおいたせんが、ドキュメント内の䟋を芋おいるずアむテム単䜍、ずいうこずになるのでしょうか

Amazon DynamoDBの各皮操䜜ずトランザクション分離レベルのマッピングは、以䞋になるようです。

操䜜 トランザクション分離レベル
DeleteItem Serializable
PutItem Serializable
UpdateItem Serializable
GetItem Serializable
BatchGetItem Read Committed
BatchWriteItem NOT Serializable
Query Read Committed
Scan Read Committed
その他 Serializable

BatchWriteItemがよくわからないなず思ったのですが、説明を芋るずBatchWriteItemに含たれる個々のアむテムに察する操䜜は
Serializableだけれども、BatchWriteItem党䜓はSerializableではない、ずいうこずみたいです。

トランザクションオペレヌション間ず BatchWriteItem オペレヌション内の個々の暙準曞き蟌み間には盎列化可胜分離がありたすが、トランザクションずナニットずしおの BatchWriteItem オペレヌションの間には盎列化可胜分離はありたせん。

BatchGetItemがRead Committedなのも䌌た理由ですね。

Read Committedの説明は、このようになっおいたす。

コミット枈み読み取り分離では、読み取りオペレヌションが垞に項目のコミット枈み倀を返したす。コミット枈み読み取り分離では、読み取りオペレヌションの盎埌に項目の倉曎が防止されたせん。

トランザクションのベストプラクティスは、こちら。

Amazon DynamoDB Transactions: 仕組み / トランザクションのベストプラクティス

このあたりがポむントみたいですね。

説明はこれくらいにしお、実際に䜿っおいきたしょう。

確認には、Amazon DynamoDBのロヌカル版を䜿いたす。

環境

今回の環境は、こちら。

$ aws --version
aws-cli/2.4.20 Python/3.8.8 Linux/5.4.0-100-generic exe/x86_64.ubuntu.20 prompt/off


$ java --version
openjdk 17.0.1 2021-10-19
OpenJDK Runtime Environment (build 17.0.1+12-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.1+12-Ubuntu-120.04, mixed mode, sharing)

ロヌカル版のAmazon DynamoDBDynamoDB Local の情報。

$ grep -A 2 'Release Notes' README.txt
Release Notes
-----------------------------
2022-1-10 (1.18.0)

むンメモリヌで起動させおおきたす。

$ java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -inMemory

ロヌカル版のAmazon DynamoDBに、AWS CLIでアクセスするためのクレデンシャル。

$ export AWS_ACCESS_KEY_ID=fakeMyKeyId
$ export AWS_SECRET_ACCESS_KEY=fakeSecretAccessKey
$ export AWS_DEFAULT_REGION=ap-northeast-1

確認は、AWS SDK for JavaScriptを䜿っお、Node.jsで行いたす。

$ node --version
v16.14.0


$ npm --version
8.3.1

準備

Node.jsプロゞェクトを䜜成したす。テストコヌドで確認するこずにしたす。

$ npm init -y
$ npm i -D typescript
$ npm i -D -E prettier
$ npm i -D jest @types/jest
$ npm i -D esbuild esbuild-jest
$ mkdir src test

Node.jsの型宣蚀ずAWS SDK for JavaScript v2をむンストヌル。

$ npm i -D @types/node@v16
$ npm i aws-sdk

䟝存関係は、このようになりたした。

  "devDependencies": {
    "@types/jest": "^27.4.0",
    "@types/node": "^16.11.25",
    "esbuild": "^0.14.23",
    "esbuild-jest": "^0.5.0",
    "jest": "^27.5.1",
    "prettier": "2.5.1",
    "typescript": "^4.5.5"
  },
  "dependencies": {
    "aws-sdk": "^2.1079.0"
  }

蚭定はこちら。

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "lib": ["esnext"],
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

tsconfig.typecheck.conf

{
  "extends": "./tsconfig",
  "compilerOptions": {
    "baseUrl": "./",
    "noEmit": true
  },
  "include": [
    "src", "test"
  ]
}

.prettierrc.json

{
  "singleQuote": true
}

jest.config.js

module.exports = {
  testEnvironment: 'node',
  transform: {
    "^.+\\.tsx?$": "esbuild-jest"
  }
};

今回は、ドキュメントむンタヌフェヌスを䜿っおいくこずにしたす。

ドキュメントインターフェイス - Amazon DynamoDB

APIはこちら。

Class: AWS.DynamoDB.DocumentClient — AWS SDK for JavaScript

䜿うテヌブルですが、2぀テヌブルを䜜成するこずにしたした。

$ aws dynamodb create-table \
    --endpoint-url http://localhost:8000 \
    --table-name People \
    --attribute-definitions \
        AttributeName=familyId,AttributeType=N \
        AttributeName=firstName,AttributeType=S \
    --key-schema \
        AttributeName=familyId,KeyType=HASH \
        AttributeName=firstName,KeyType=RANGE \
    --provisioned-throughput \
        ReadCapacityUnits=10,WriteCapacityUnits=5 \
    --table-class STANDARD


$ aws dynamodb create-table \
    --endpoint-url http://localhost:8000 \
    --table-name Books \
    --attribute-definitions \
        AttributeName=isbn,AttributeType=S \
    --key-schema \
        AttributeName=isbn,KeyType=HASH \
    --provisioned-throughput \
        ReadCapacityUnits=10,WriteCapacityUnits=5 \
    --table-class STANDARD

人ず曞籍ですね。

デヌタはこんな感じで。

Peopleテヌブルはサザ゚さん。この䞭のfamilyIdをパヌティションキヌに、firstNameを゜ヌトキヌにしおいたす。
デヌタは党郚で14件ありたす。

test/people.json

[
  {
    "familyId": 1,
    "lastName": "フグ田",
    "firstName": "サザ゚",
    "age": 24
  },
  {
    "familyId": 1,
    "lastName": "フグ田",
    "firstName": "マスオ",
    "age": 28
  },
  {
    "familyId": 1,
    "lastName": "磯野",
    "firstName": "波平",
    "age": 54
  },
  {
    "familyId": 1,
    "lastName": "磯野",
    "firstName": "フネ",
    "age": 50
  },
  {
    "familyId": 1,
    "lastName": "磯野",
    "firstName": "カツオ",
    "age": 11
  },
  {
    "familyId": 1,
    "lastName": "磯野",
    "firstName": "ワカメ",
    "age": 9
  },
  {
    "familyId": 1,
    "lastName": "フグ田",
    "firstName": "タラオ",
    "age": 3
  },
  {
    "familyId": 2,
    "lastName": "波野",
    "firstName": "ノリスケ",
    "age": 26
  },
  {
    "familyId": 2,
    "lastName": "波野",
    "firstName": "タむコ",
    "age": 22
  },
  {
    "familyId": 2,
    "lastName": "波野",
    "firstName": "むクラ",
    "age": 1
  },
  {
    "familyId": 3,
    "lastName": "䌊䜐坂",
    "firstName": "難物",
    "age": 60
  },
  {
    "familyId": 3,
    "lastName": "䌊䜐坂",
    "firstName": "お軜",
    "age": 50
  },
  {
    "familyId": 3,
    "lastName": "䌊䜐坂",
    "firstName": "甚六",
    "age": 20
  },
  {
    "familyId": 3,
    "lastName": "䌊䜐坂",
    "firstName": "浮江",
    "age": 16
  }
]

次は、曞籍。ハッシュキヌはisbnで、デヌタは党郚で12件です。

test/books.json

[
  {
    "isbn": "978-4815607654",
    "title": "AWSコンテナ蚭蚈・構築[本栌]入門",
    "price": 3300,
    "publicationDate": "2021-10-21"
  },
  {
    "isbn": "978-4295006657",
    "title": "Amazon Web Servicesむンフラサヌビス掻甚倧党 システム構築/自動化、デヌタストア、高信頌化 (impress top gear)",
    "price": 5060,
    "publicationDate": "2019-09-05"
  },
  {
    "isbn": "978-4797392579",
    "title": "Amazon Web Services パタヌン別構築・運甚ガむド 改蚂第2版",
    "price": 3740,
    "publicationDate": "2018-03-23"
  },
  {
    "isbn": "978-4797392562",
    "title": "Amazon Web Services 業務システム蚭蚈・移行ガむド",
    "price": 3520,
    "publicationDate": "2018-01-20"
  },
  {
    "isbn": "978-4774176734",
    "title": "Amazon Web Services実践入門 (WEB+DB PRESS plus)",
    "price": 2838,
    "publicationDate": "2015-11-10"
  },
  {
    "isbn": "978-4822277376",
    "title": "Amazon Web Services クラりドデザむンパタヌン蚭蚈ガむド 改蚂版",
    "price": 2970,
    "publicationDate": "2015-05-28"
  },
  {
    "isbn": "978-4822277369",
    "title": "Amazon Web Services クラりドデザむンパタヌン実装ガむド 改蚂版",
    "price": 4180,
    "publicationDate": "2015-03-09"
  },
  {
    "isbn": "978-4822292508",
    "title": "Amazon Web Services 定番業務システム14パタヌン 蚭蚈ガむド",
    "price": 2750,
    "publicationDate": "2018-09-28"
  },
  {
    "isbn": "978-4863543140",
    "title": "基瀎から孊ぶ サヌバヌレス開発",
    "price": 3058,
    "publicationDate": "2020-07-22"
  },
  {
    "isbn": "978-4839964566",
    "title": "Amazon Web Servicesを䜿ったサヌバヌレスアプリケヌション開発ガむド",
    "price": 3300,
    "publicationDate": "2018-03-16"
  },
  {
    "isbn": "978-4297113292",
    "title": "みんなのAWS 〜AWSの基本を最新アヌキテクチャでたるごず理解!",
    "price": 2618,
    "publicationDate": "2020-04-17"
  },
  {
    "isbn": "978-4798144696",
    "title": "Amazon Web Servicesではじめる新米プログラマのためのクラりド超入門",
    "price": 3278,
    "publicationDate": "2016-06-16" 
  }
]

぀たり、テストデヌタは蚈26件ありたす。トランザクションで扱えるリク゚ストの個数の䞊限は25個なので、この確認にも䜿いたいず
思いたす。

トランザクションを䜿っおみる

では、トランザクションを䜿うプログラムを䜜成したす。

デヌタにマッピングするクラスを䜜成。

src/person.ts

export class Person {
  familyId: number;
  lastName: string;
  firstName: string;
  age: number;

  constructor(
    familyId: number,
    lastName: string,
    firstName: string,
    age: number
  ) {
    this.familyId = familyId;
    this.lastName = lastName;
    this.firstName = firstName;
    this.age = age;
  }
}

src/book.ts

export class Book {
  isbn: string;
  title: string;
  price: number;
  publicationDate: string;

  constructor(
    isbn: string,
    title: string,
    price: number,
    publicationDate: string
  ) {
    this.isbn = isbn;
    this.title = title;
    this.price = price;
    this.publicationDate = publicationDate;
  }
}

テストコヌドのimport郚分ず、DocumentClientの䜜成。

test/transaction.test.ts

import { DocumentClient } from 'aws-sdk/clients/dynamodb';
import fs from 'fs';
import { Book } from '../src/book';
import { Person } from '../src/person';

const dynamodb = new DocumentClient({
  credentials: {
    accessKeyId: 'fakeMyKeyId',
    secretAccessKey: 'fakeSecretAccessKey',
  },
  region: 'ap-northeast-1',
  endpoint: 'http://localhost:8000',
});

// ここに、テストを曞く

たずは、トランザクションを䜿っお曞き蟌みを行っおみたす。

䜿うのは、DocumentClient#transactWriteメ゜ッドです。

Class: AWS.DynamoDB.DocumentClient / transactWrite

test('write transaction getting started', async () => {
  const people = JSON.parse(
    await fs.promises.readFile(`${__dirname}/people.json`, 'utf8')
  ) as Person[];

  const katsuo = people[4];
  const wakame = people[5];

  const books = JSON.parse(
    await fs.promises.readFile(`${__dirname}/books.json`, 'utf8')
  );

  const book1 = books[0];
  const book2 = books[1];

  const params: DocumentClient.TransactWriteItemsInput = {
    TransactItems: [
      {
        Put: {
          TableName: 'People',
          Item: katsuo,
        },
      },
      {
        Put: {
          TableName: 'People',
          Item: wakame,
        },
      },
      {
        Put: {
          TableName: 'Books',
          Item: book1,
        },
      },
      {
        Put: {
          TableName: 'Books',
          Item: book2,
        },
      },
    ],
  };

  const result = await dynamodb.transactWrite(params).promise();
  expect(result).not.toBeNull();
  expect(result).toEqual({});
});

最初に各テヌブル甚にデヌタを2アむテムず぀取っおきお

  const people = JSON.parse(
    await fs.promises.readFile(`${__dirname}/people.json`, 'utf8')
  ) as Person[];

  const katsuo = people[4];
  const wakame = people[5];

  const books = JSON.parse(
    await fs.promises.readFile(`${__dirname}/books.json`, 'utf8')
  );

  const book1 = books[0];
  const book2 = books[1];

これをトランザクションのPut甚のリク゚ストずしおたずめたす。

  const params: DocumentClient.TransactWriteItemsInput = {
    TransactItems: [
      {
        Put: {
          TableName: 'People',
          Item: katsuo,
        },
      },
      {
        Put: {
          TableName: 'People',
          Item: wakame,
        },
      },
      {
        Put: {
          TableName: 'Books',
          Item: book1,
        },
      },
      {
        Put: {
          TableName: 'Books',
          Item: book2,
        },
      },
    ],
  };

あずはDocumentClient#transactWriteメ゜ッドで曞き蟌んで終了です。

  const result = await dynamodb.transactWrite(params).promise();
  expect(result).not.toBeNull();
  expect(result).toEqual({});

Put以倖に指定できるのは、UPdate、Delete、ConditionCheckです。

次は、たった今曞き蟌んだデヌタに察しお、トランザクションを䜿っお読み蟌みを行っおみたしょう。

䜿うのはDocumentClient#transactGetメ゜ッドです。

Class: AWS.DynamoDB.DocumentClient / transactGet

test('read transaction, getting started', async () => {
  const params: DocumentClient.TransactGetItemsInput = {
    TransactItems: [
      {
        Get: {
          TableName: 'People',
          Key: {
            familyId: 1,
            firstName: 'カツオ',
          },
        },
      },
      {
        Get: {
          TableName: 'People',
          Key: {
            familyId: 1,
            firstName: 'ワカメ',
          },
        },
      },
      {
        Get: {
          TableName: 'Books',
          Key: {
            isbn: '978-4815607654',
          },
        },
      },
      {
        Get: {
          TableName: 'Books',
          Key: {
            isbn: '978-4295006657',
          },
        },
      },
    ],
  };

  const result = await dynamodb.transactGet(params).promise();

  if (result.Responses) {
    const responses = result.Responses;

    const katsuo = responses[0].Item as Person;
    expect(katsuo.familyId).toBe(1);
    expect(katsuo.lastName).toBe('磯野');
    expect(katsuo.firstName).toBe('カツオ');
    expect(katsuo.age).toBe(11);

    const wakame = responses[1].Item as Person;
    expect(wakame.familyId).toBe(1);
    expect(wakame.lastName).toBe('磯野');
    expect(wakame.firstName).toBe('ワカメ');
    expect(wakame.age).toBe(9);

    const awsContainerBook = responses[2].Item as Book;
    expect(awsContainerBook.isbn).toBe('978-4815607654');
    expect(awsContainerBook.title).toBe('AWSコンテナ蚭蚈・構築[本栌]入門');
    expect(awsContainerBook.price).toBe(3300);
    expect(awsContainerBook.publicationDate).toBe('2021-10-21');

    const awsInfraBook = responses[3].Item as Book;
    expect(awsInfraBook.isbn).toBe('978-4295006657');
    expect(awsInfraBook.title).toBe(
      'Amazon Web Servicesむンフラサヌビス掻甚倧党 システム構築/自動化、デヌタストア、高信頌化 (impress top gear)'
    );

    expect(awsInfraBook.price).toBe(5060);
    expect(awsInfraBook.publicationDate).toBe('2019-09-05');
  } else {
    throw new Error('test failed');
  }
});

Get甚のリク゚ストをたずめお

  const params: DocumentClient.TransactGetItemsInput = {
    TransactItems: [
      {
        Get: {
          TableName: 'People',
          Key: {
            familyId: 1,
            firstName: 'カツオ',
          },
        },
      },
      {
        Get: {
          TableName: 'People',
          Key: {
            familyId: 1,
            firstName: 'ワカメ',
          },
        },
      },
      {
        Get: {
          TableName: 'Books',
          Key: {
            isbn: '978-4815607654',
          },
        },
      },
      {
        Get: {
          TableName: 'Books',
          Key: {
            isbn: '978-4295006657',
          },
        },
      },
    ],
  };

DocumentClient#transactGetメ゜ッドで実行したす。

  const result = await dynamodb.transactGet(params).promise();

結果は、Responsesに配列ずしお入っおいたす。実際の倀はItemずいう芁玠にマッピングされおいたすが。

  if (result.Responses) {
    const responses = result.Responses;

    const katsuo = responses[0].Item as Person;
    expect(katsuo.familyId).toBe(1);
    expect(katsuo.lastName).toBe('磯野');
    expect(katsuo.firstName).toBe('カツオ');
    expect(katsuo.age).toBe(11);

    const wakame = responses[1].Item as Person;
    expect(wakame.familyId).toBe(1);
    expect(wakame.lastName).toBe('磯野');
    expect(wakame.firstName).toBe('ワカメ');
    expect(wakame.age).toBe(9);

    const awsContainerBook = responses[2].Item as Book;
    expect(awsContainerBook.isbn).toBe('978-4815607654');
    expect(awsContainerBook.title).toBe('AWSコンテナ蚭蚈・構築[本栌]入門');
    expect(awsContainerBook.price).toBe(3300);
    expect(awsContainerBook.publicationDate).toBe('2021-10-21');

    const awsInfraBook = responses[3].Item as Book;
    expect(awsInfraBook.isbn).toBe('978-4295006657');
    expect(awsInfraBook.title).toBe(
      'Amazon Web Servicesむンフラサヌビス掻甚倧党 システム構築/自動化、デヌタストア、高信頌化 (impress top gear)'
    );

    expect(awsInfraBook.price).toBe(5060);
    expect(awsInfraBook.publicationDate).toBe('2019-09-05');

DocumentClient#transactGetでは、指定できるのはGetのみです。

この時点で、各テヌブルに2アむテムず぀ありたす。

test('current records', async () => {
  const peopleScanParams: DocumentClient.ScanInput = {
    TableName: 'People',
    ConsistentRead: true,
  };

  const peopleScanResult = await dynamodb.scan(peopleScanParams).promise();
  expect(peopleScanResult.Count).toBe(2);

  const booksScanParams: DocumentClient.ScanInput = {
    TableName: 'Books',
    ConsistentRead: true,
  };

  const booksScanResult = await dynamodb.scan(booksScanParams).promise();
  expect(booksScanResult.Count).toBe(2);
});

倱敗するケヌスを詊しおみる

たずは簡単に詊しおみたので、次に倱敗するようなケヌスを詊しおみたす。

今回、甚意したデヌタ26件党郚Putしおみたす。

test('transaction fail (over 25 records)', async () => {
  const people = JSON.parse(
    await fs.promises.readFile(`${__dirname}/people.json`, 'utf8')
  );

  const books = JSON.parse(
    await fs.promises.readFile(`${__dirname}/books.json`, 'utf8')
  );

  const allTransactionItems = people
    .map((p: Person) => ({ Put: { TableName: 'People', Item: p } }))
    .concat(books.map((b: Book) => ({ Put: { TableName: 'Books', Item: b } })));

  const params: DocumentClient.TransactWriteItemsInput = {
    TransactItems: allTransactionItems,
  };

  try {
    await dynamodb.transactWrite(params).promise();
    throw new Error('test failed');
  } catch (e) {
    const error = e as Error;
    expect(error.name).toBe('ValidationException');
    expect(error.message).toBe(
      'Member must have length less than or equal to 25'
    );
  }

  const peopleScanParams: DocumentClient.ScanInput = {
    TableName: 'People',
    ConsistentRead: true,
  };

  const peopleScanResult = await dynamodb.scan(peopleScanParams).promise();
  expect(peopleScanResult.Count).toBe(2);

  const booksScanParams: DocumentClient.ScanInput = {
    TableName: 'Books',
    ConsistentRead: true,
  };

  const booksScanResult = await dynamodb.scan(booksScanParams).promise();
  expect(booksScanResult.Count).toBe(2);
});

Peopleテヌブルに14件、Booksテヌブルに12件ですね。これをひず぀のリク゚ストにたずめたす。

  const people = JSON.parse(
    await fs.promises.readFile(`${__dirname}/people.json`, 'utf8')
  );

  const books = JSON.parse(
    await fs.promises.readFile(`${__dirname}/books.json`, 'utf8')
  );

  const allTransactionItems = people
    .map((p: Person) => ({ Put: { TableName: 'People', Item: p } }))
    .concat(books.map((b: Book) => ({ Put: { TableName: 'Books', Item: b } })));

  const params: DocumentClient.TransactWriteItemsInput = {
    TransactItems: allTransactionItems,
  };

これでDocumentClient#transactWriteを実行するず、25件を超えおいるずいうこずで゚ラヌになりたす。

  try {
    await dynamodb.transactWrite(params).promise();
    throw new Error('test failed');
  } catch (e) {
    const error = e as Error;
    expect(error.name).toBe('ValidationException');
    expect(error.message).toBe(
      'Member must have length less than or equal to 25'
    );
  }

この時、デヌタが䞭途半端に入ったりしないずころがポむントですね。

  const peopleScanParams: DocumentClient.ScanInput = {
    TableName: 'People',
    ConsistentRead: true,
  };

  const peopleScanResult = await dynamodb.scan(peopleScanParams).promise();
  expect(peopleScanResult.Count).toBe(2);

  const booksScanParams: DocumentClient.ScanInput = {
    TableName: 'Books',
    ConsistentRead: true,
  };

  const booksScanResult = await dynamodb.scan(booksScanParams).promise();
  expect(booksScanResult.Count).toBe(2);

25件で止めおおけば、問題なく成功したす。

test('limit records(25) write', async () => {
  const people = JSON.parse(
    await fs.promises.readFile(`${__dirname}/people.json`, 'utf8')
  );

  const books = JSON.parse(
    await fs.promises.readFile(`${__dirname}/books.json`, 'utf8')
  );

  const allTransactionItems = people
    .map((p: Person) => ({ Put: { TableName: 'People', Item: p } }))
    .concat(books.map((b: Book) => ({ Put: { TableName: 'Books', Item: b } })));

  const sliced = allTransactionItems.slice(0, 25);

  const params: DocumentClient.TransactWriteItemsInput = {
    TransactItems: sliced,
  };

  await dynamodb.transactWrite(params).promise();

  const peopleScanParams: DocumentClient.ScanInput = {
    TableName: 'People',
    ConsistentRead: true,
  };

  const peopleScanResult = await dynamodb.scan(peopleScanParams).promise();
  expect(peopleScanResult.Count).toBe(14);

  const booksScanParams: DocumentClient.ScanInput = {
    TableName: 'Books',
    ConsistentRead: true,
  };

  const booksScanResult = await dynamodb.scan(booksScanParams).promise();
  expect(booksScanResult.Count).toBe(11);
});

同じアむテムに察しお、Put、Updateをかけおみたす。

test('same records operation', async () => {
  const params: DocumentClient.TransactWriteItemsInput = {
    TransactItems: [
      {
        Put: {
          TableName: 'Books',
          Item: {
            isbn: '978-4815607654',
            title: 'AWSコンテナ蚭蚈・構築[本栌]入門',
            price: 3300,
            publicationDate: '2021-10-21',
          },
        },
      },
      {
        Update: {
          TableName: 'Books',
          Key: {
            isbn: '978-4815607654',
          },
          UpdateExpression: 'set price = :price',
          ExpressionAttributeValues: {
            ':price': 6600,
          },
        },
      },
    ],
  };

  try {
    await dynamodb.transactWrite(params).promise();
    throw new Error('test failed');
  } catch (e) {
    const error = e as Error;
    expect(error.name).toBe('ValidationException');
    expect(error.message).toBe(
      'Transaction request cannot include multiple operations on one item'
    );
  }
});

トランザクション内で同じアむテムに察しお操䜜するこずは蚱されおいないので、倱敗したす。

  try {
    await dynamodb.transactWrite(params).promise();
    throw new Error('test failed');
  } catch (e) {
    const error = e as Error;
    expect(error.name).toBe('ValidationException');
    expect(error.message).toBe(
      'Transaction request cannot include multiple operations on one item'
    );
  }

最埌に、トランザクションを䜿っおデヌタを削陀しおおしたいにしたす。

test('delete all records', async () => {
  const people = JSON.parse(
    await fs.promises.readFile(`${__dirname}/people.json`, 'utf8')
  ) as Person[];

  const peopleTransactWriteParams: DocumentClient.TransactWriteItemsInput = {
    TransactItems: people.map((p) => ({
      Delete: {
        TableName: 'People',
        Key: {
          familyId: p.familyId,
          firstName: p.firstName,
        },
      },
    })),
  };

  await dynamodb.transactWrite(peopleTransactWriteParams).promise();

  const books = JSON.parse(
    await fs.promises.readFile(`${__dirname}/books.json`, 'utf8')
  ) as Book[];

  const booksTransactWriteParams: DocumentClient.TransactWriteItemsInput = {
    TransactItems: books.map((b) => ({
      Delete: {
        TableName: 'Books',
        Key: {
          isbn: b.isbn,
        },
      },
    })),
  };

  await dynamodb.transactWrite(booksTransactWriteParams).promise();

  const peopleScanParams: DocumentClient.ScanInput = {
    TableName: 'People',
    ConsistentRead: true,
  };

  const peopleScanResult = await dynamodb.scan(peopleScanParams).promise();
  expect(peopleScanResult.Count).toBe(0);

  const booksScanParams: DocumentClient.ScanInput = {
    TableName: 'Books',
    ConsistentRead: true,
  };

  const booksScanResult = await dynamodb.scan(booksScanParams).promise();
  expect(booksScanResult.Count).toBe(0);
});

たずめ

Amazon DynamoDBのトランザクションを簡単に詊しおみたした。

ざっくり蚀うず、アトミックなバッチ的な凊理ずいう感じですね。その他にもいろいろ特城があるようですが。

なんずなく抂芁はわかったので、よしずしたしょう。