このガイドは、JavaScript で Amazon DynamoDB を使用したいと考えているプログラマーを対象としています。AWS SDK for JavaScript、利用可能な抽象化レイヤー、接続の設定、エラー処理、再試行ポリシーの定義、キープアライブの管理などについて説明します。
トピック
AWS SDK for JavaScript について
AWS SDK for JavaScript を使用すると、ブラウザスクリプトまたは Node.js のいずれかで AWS のサービス にアクセスできます。このドキュメントでは、最新バージョンの SDK (V3) を主に取り上げます。AWS SDK for JavaScript V3 は、AWS が管理するオープンソースのプロジェクトであり、GitHub でホスト
JavaScript V2 は V3 と似ていますが、構文が違います。V3 の方がモジュール性が高いため、依存関係をより小さくして配布することが容易で、TypeScript のサポートも充実しています。最新バージョンの SDK を使用することをお勧めします。
AWS SDK for JavaScript V3 を使用する
Node Package Manager を使用して SDK を Node.js アプリケーションに追加できます。以下の例は、DynamoDB の操作に一般的に使われる SDK パッケージの追加方法を示しています。
-
npm install @aws-sdk/client-dynamodb
-
npm install @aws-sdk/lib-dynamodb
-
npm install @aws-sdk/util-dynamodb
パッケージをインストールすると、package.json プロジェクトファイルの依存関係セクションに参照が追加されます。新しい ECMAScript モジュール構文を使用することもできます。これら 2 つのアプローチの詳細については、「考慮事項」セクションを参照してください。
JavaScript のドキュメントを参照する
まずは、JavaScript のドキュメントを確認しましょう。以下のリソースを参照してください。
-
JavaScript の主要ドキュメントについては、「デベロッパーガイド」を参照してください。インストール手順は「Setting up」セクションに記載されています。
-
API リファレンスドキュメントを参照し、使用可能なすべてのクラスとメソッドを確認してください。
-
SDK for JavaScript は DynamoDB 以外にも数多くの AWS のサービス をサポートしています。以下の手順にそって、DynamoDB の特定の API カバレッジを調べてください。
-
[Services] から [DynamoDB and Libraries] を選択します。これは、低レベルクライアントをドキュメント化したものです。
-
lib-dynamodb を選択します。これは、高レベルクライアントをドキュメント化したものです。これら 2 つのクライアントは、2 つの異なる抽象化レイヤーを表しています。任意で選んで使用できます。抽象レイヤーの詳細については、以下のセクションを参照してください。
-
抽象化レイヤー
SDK for JavaScript V3 には、低レベルのクライアント (DynamoDBClient
) と高レベルのクライアント (DynamoDBDocumentClient
) があります。
低レベルクライアント (DynamoDBClient
)
低レベルクライアントでは、基盤のワイヤプロトコルが別段抽象化されません。通信をあらゆる側面から完全に制御できますが、抽象化されていないため、項目定義の提供などの操作は、DynamoDB JSON 形式を使って行う必要があります。
以下の例に示すように、この形式ではデータ型を明示的に指定する必要があります。S は文字列値を示し、N は数値を示します。ネットワーク上の数値は、精度が損なわれないように、必ず数値型としてタグ付けされた文字列として送信されます。低レベルの API コールには、PutItemCommand
や GetItemCommand
などの命名規則があります。
次の例では、低レベルクライアントを使用し、DynamoDB JSON で Item
を定義しています。
const { DynamoDBClient, PutItemCommand } = require("@aws-sdk/client-dynamodb");
const client = new DynamoDBClient({});
async function addProduct() {
const params = {
TableName: "products",
Item: {
"id": { S: "Product01" },
"description": { S: "Hiking Boots" },
"category": { S: "footwear" },
"sku": { S: "hiking-sku-01" },
"size": { N: "9" }
}
};
try {
const data = await client.send(new PutItemCommand(params));
console.log('result : ' + JSON.stringify(data));
} catch (error) {
console.error("Error:", error);
}
}
addProduct();
高レベルクライアント (DynamoDBDocumentClient
)
高レベルの DynamoDB ドキュメントクライアントには、データを手動でマーシャリングする必要がない、標準の JavaScript オブジェクトを使用して直接読み書きできるなど、便利な機能が組み込まれています。lib-dynamodb
のドキュメントには、利点が一覧で紹介されています。
DynamoDBDocumentClient
をインスタンス化するには、まず、低レベルの DynamoDBClient
を構築し、それを DynamoDBDocumentClient
でラップします。関数の命名規則は 2 つのパッケージ間で若干異なります。例えば、低レベルでは PutItemCommand
を使用し、高レベルでは PutCommand
を使用します。名前が違えば、両方の関数セットが同じコンテキストで共存可能です。つまり、同一スクリプト内で両方を組み合わせることができます。
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const { DynamoDBDocumentClient, PutCommand } = require("@aws-sdk/lib-dynamodb");
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
async function addProduct() {
const params = {
TableName: "products",
Item: {
id: "Product01",
description: "Hiking Boots",
category: "footwear",
sku: "hiking-sku-01",
size: 9,
},
};
try {
const data = await docClient.send(new PutCommand(params));
console.log('result : ' + JSON.stringify(data));
} catch (error) {
console.error("Error:", error);
}
}
addProduct();
GetItem
、Query
、Scan
などの API オペレーションを使用して項目を読み取る場合の使用パターンは同じです。
marshall ユーティリティ関数を使用する
低レベルクライアントを使用して、データ型を自分でマーシャリングまたはアンマーシャリングできます。ユーティリティパッケージ util-dynamodb には、JSON を受け入れて DynamoDB JSON を生成する marshall()
ユーティリティ関数と、その逆を行う unmarshall()
関数があります。次の例では、低レベルクライアントを使用し、marshall()
を呼び出してデータマーシャリングを処理しています。
const { DynamoDBClient, PutItemCommand } = require("@aws-sdk/client-dynamodb");
const { marshall } = require("@aws-sdk/util-dynamodb");
const client = new DynamoDBClient({});
async function addProduct() {
const params = {
TableName: "products",
Item: marshall({
id: "Product01",
description: "Hiking Boots",
category: "footwear",
sku: "hiking-sku-01",
size: 9,
}),
};
try {
const data = await client.send(new PutItemCommand(params));
} catch (error) {
console.error("Error:", error);
}
}
addProduct();
項目の読み込み
DynamoDB から項目を 1 つ読み込むには、GetItem
API オペレーションを使用します。PutItem
コマンドと同様に、低レベルのクライアントまたは高レベルのドキュメントクライアントのいずれかを選んで使用できます。以下の例は、高レベルのドキュメントクライアントを使用して項目を取得する方法を示しています。
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const { DynamoDBDocumentClient, GetCommand } = require("@aws-sdk/lib-dynamodb");
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
async function getProduct() {
const params = {
TableName: "products",
Key: {
id: "Product01",
},
};
try {
const data = await docClient.send(new GetCommand(params));
console.log('result : ' + JSON.stringify(data));
} catch (error) {
console.error("Error:", error);
}
}
getProduct();
Query
API オペレーションを使用して、複数の項目を読み込みます。低レベルのクライアントまたはドキュメントクライアントを使用できます。以下の例では、高レベルのドキュメントクライアントを使用しています。
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const {
DynamoDBDocumentClient,
QueryCommand,
} = require("@aws-sdk/lib-dynamodb");
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
async function productSearch() {
const params = {
TableName: "products",
IndexName: "GSI1",
KeyConditionExpression: "#category = :category and begins_with(#sku, :sku)",
ExpressionAttributeNames: {
"#category": "category",
"#sku": "sku",
},
ExpressionAttributeValues: {
":category": "footwear",
":sku": "hiking",
},
};
try {
const data = await docClient.send(new QueryCommand(params));
console.log('result : ' + JSON.stringify(data));
} catch (error) {
console.error("Error:", error);
}
}
productSearch();
条件付きの書き込み
DynamoDB の書き込みオペレーションでは、論理条件式を指定できます。条件式が true と評価されないと、書き込みは続行されません。条件の評価が true にならなかった場合は、例外が生成されます。条件式では、該当する項目が既に存在するかどうかや、その属性が特定の制約に一致するかどうかを確認できます。
ConditionExpression = "version = :ver AND size(VideoClip) < :maxsize"
条件式が失敗した場合は、ReturnValuesOnConditionCheckFailure
を使用して、条件を満たさなかった項目をエラーレスポンスに含めるようにリクエストできるため、問題の原因究明に役立ちます。詳細については、「Handle conditional write errors in high concurrency scenarios with Amazon DynamoDB
try {
const response = await client.send(new PutCommand({
TableName: "YourTableName",
Item: item,
ConditionExpression: "attribute_not_exists(pk)",
ReturnValuesOnConditionCheckFailure: "ALL_OLD"
}));
} catch (e) {
if (e.name === 'ConditionalCheckFailedException') {
console.log('Item already exists:', e.Item);
} else {
throw e;
}
}
JavaScript SDK V3 のその他の使用法を示す追加のコード例は、JavaScript SDK V3 ドキュメントと DynamoDB-SDK-Examples GitHub リポジトリ
ページ分割
Scan
や Query
などの読み取りリクエストでは、データセット内の複数の項目が返される場合があります。Limit
パラメータを指定して Scan
や Query
を実行した場合は、システムで多数の項目が全部読み取られた後、一部のレスポンスが送信されます。追加の項目を取得するには、ページ分割が必要です。
システムは、1 回のリクエストにつき最大 1 MB のデータのみを読み取ります。Filter
式を含めた場合も、システムは最大 1 MB のデータをディスクから読み取りますが、その 1 MB の中から、指定したフィルターに一致した項目だけを返します。フィルターオペレーションでは 1 ページあたりに返される項目数が 0 個になる場合もありますが、それでも、さらにページ分割をしないと、最後まで検索できません。
データの取得を継続するには、レスポンスで LastEvaluatedKey
を探し、それを後続のリクエストの ExclusiveStartKey
パラメータに指定する必要があります。これが、以下の例に示すように、ブックマークの役割を果たします。
注記
この例では、1 回目の反復で null lastEvaluatedKey
が ExclusiveStartKey
として渡されています。これは許容されています。
LastEvaluatedKey
を使用する例:
const { DynamoDBClient, ScanCommand } = require("@aws-sdk/client-dynamodb");
const client = new DynamoDBClient({});
async function paginatedScan() {
let lastEvaluatedKey;
let pageCount = 0;
do {
const params = {
TableName: "products",
ExclusiveStartKey: lastEvaluatedKey,
};
const response = await client.send(new ScanCommand(params));
pageCount++;
console.log(`Page ${pageCount}, Items:`, response.Items);
lastEvaluatedKey = response.LastEvaluatedKey;
} while (lastEvaluatedKey);
}
paginatedScan().catch((err) => {
console.error(err);
});
便利な paginateScan
メソッドを使用する
SDK には、paginateScan
と paginateQuery
という便利なメソッドがあります。これらは、ページ分割を自動で行い、繰り返しのリクエストをバックグラウンドで処理してくれます。標準の Limit
パラメータを使用して、1 回のリクエストで読み取る項目の最大数を指定します。
const { DynamoDBClient, paginateScan } = require("@aws-sdk/client-dynamodb");
const client = new DynamoDBClient({});
async function paginatedScanUsingPaginator() {
const params = {
TableName: "products",
Limit: 100
};
const paginator = paginateScan({client}, params);
let pageCount = 0;
for await (const page of paginator) {
pageCount++;
console.log(`Page ${pageCount}, Items:`, page.Items);
}
}
paginatedScanUsingPaginator().catch((err) => {
console.error(err);
});
注記
テーブルが小さくない限り、テーブル全体のスキャンを定期的に実行することは、アクセスパターンとして推奨されません。
構成を指定する
DynamoDBClient
を設定する際に、構成オブジェクトをコンストラクタに渡すことで、さまざまな構成オーバーライドを指定できます。例えば、接続先のリージョン (呼び出し側のコンテキストが把握していない場合) や、使用するエンドポイント URL を指定できます。開発目的で DynamoDB Local インスタンスをターゲットにする場合に便利です。
const client = new DynamoDBClient({
region: "eu-west-1",
endpoint: "http://localhost:8000",
});
タイムアウトの設定
DynamoDB では、クライアント/サーバー通信に HTTPS を使用します。HTTP レイヤーの一部の側面は、NodeHttpHandler
オブジェクトを指定することで制御できます。例えば、主要なタイムアウト値である connectionTimeout
や requestTimeout
を調整できます。connectionTimeout
は、クライアントが接続を試みる際の最長待機時間 (ミリ秒単位) です。この時間内に確立できない場合は、接続を断念します。
requestTimeout
では、リクエストが送信されてからクライアントがレスポンスを待機する時間 (ミリ秒単位) を定義します。いずれもデフォルト値は 0 です。その場合、タイムアウトは無効になり、レスポンスが届かなければクライアントは制限なく待ち続けることになります。ネットワークに問題が発生した場合にリクエストがエラーになり、新しいリクエストを開始できるように、妥当なタイムアウト値を設定しておいた方が賢明です。例:
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { NodeHttpHandler } from "@smithy/node-http-handler";
const requestHandler = new NodeHttpHandler({
connectionTimeout: 2000,
requestTimeout: 2000,
});
const client = new DynamoDBClient({
requestHandler
});
注記
この例では Smithy
タイムアウト値を設定するほかに、最大ソケット数を設定し、オリジンあたりの同時接続数を増やすことができます。デベロッパーガイドでは、maxSockets
パラメータの設定について詳しく解説しています。
キープアライブの設定
HTTPS を使用する場合、最初のリクエストでは、安全な接続を確立するために何往復かの通信が必要になります。HTTP キープアライブを使用すると、すでに確立された接続を後続のリクエストで再利用できるため、リクエストの効率が上がり、レイテンシーが短縮されます。JavaScript V3 では、HTTP キープアライブがデフォルトで有効になっています。
アイドル状態の接続を維持できる時間には制限があります。接続がアイドル状態になるが、すでに確立されている接続を次のリクエストで利用したい場合は、定期的に (たとえば 1 分ごとに) リクエストを送信することを検討してください。
注記
SDK の旧バージョン V2 では、キープアライブはデフォルトで無効になっているため、各接続は使用後すぐに切断されます。V2 を使用している場合は、この設定をオーバーライドできます。
再試行の設定
SDK がエラーレスポンスを受信し、そのエラーを SDK が再開可能であると判断した場合 (スロットリング例外や一時的なサービス例外など)、再試行されます。呼び出し側にはわからない形で行われますが、リクエスト成功までの時間が長引くという症状は現れます。
SDK for JavaScript V3 は、デフォルトではリクエストを合計 3 回行います。それで成功しなければそれ以上は再試行せず、呼び出し側のコンテキストにエラーを渡します。こうした再試行の回数と頻度を調整できます。
DynamoDBClient
コンストラクタには、試行回数を制限する maxAttempts
を設定できます。以下の例では、その値がデフォルトの 3 から合計 5 に引き上げられています。0 または 1 に設定した場合、自動再試行は不要であり、再開可能なエラーはキャッチブロック内で手動で処理するという意味になります。
const client = new DynamoDBClient({
maxAttempts: 5,
});
また、再試行のタイミングも、カスタムの再試行戦略で制御できます。その場合は、util-retry
ユーティリティパッケージをインポートし、現在の再試行が何回目かに応じて再試行の間隔を計算するカスタムのバックオフ関数を作成します。
以下の例では、1 回目の試行が失敗した場合は、15、30、90、360 ミリ秒と徐々に遅延を増やしながら最大 5 回試行するように指定しています。カスタムのバックオフ関数 calculateRetryBackoff
は、再試行回数 (初回の再試行が 1 で、以降増分) に応じて遅延を計算し、該当するリクエストで待機するミリ秒数を返します。
const { ConfiguredRetryStrategy } = require("@aws-sdk/util-retry");
const calculateRetryBackoff = (attempt) => {
const backoffTimes = [15, 30, 90, 360];
return backoffTimes[attempt - 1] || 0;
};
const client = new DynamoDBClient({
retryStrategy: new ConfiguredRetryStrategy(
5, // max attempts.
calculateRetryBackoff // backoff function.
),
});
ウェイター
DynamoDB クライアントには、便利なウェーター関数waitUntilTableExists
関数を呼び出すと、テーブルが ACTIVE になるまでコードがブロックされます。ウェーターは内部的に 20 秒ごとに describe-table
を実行し、DynamoDB サービスをポーリングします。
import {waitUntilTableExists, waitUntilTableNotExists} from "@aws-sdk/client-dynamodb";
… <create table details>
const results = await waitUntilTableExists({client: client, maxWaitTime: 180}, {TableName: "products"});
if (results.state == 'SUCCESS') {
return results.reason.Table
}
console.error(`${results.state} ${results.reason}`);
waitUntilTableExists
関数は、describe-table
コマンドを実行でき、テーブルのステータスが ACTIVE と表示された場合にのみ制御を返します。そのため、waitUntilTableExists
を使用して、テーブルの作成や、GSI インデックスの追加などの変更の完了を待つことができます。こうした変更には時間がかかり、終わってからテーブルが ACTIVE ステータスに戻ります。
エラー処理
ここで最初に紹介した数例では、すべてのエラーを網羅してキャッチしています。しかし、実際のアプリケーションでは、さまざまなエラータイプを見分け、より緻密なエラー処理を実装することが重要です。
DynamoDB エラーレスポンスには、エラーの名前を含むメタデータが含まれています。エラーをキャッチし、エラー条件の文字列名候補と照合して、処理方法を決定できます。サーバー側のエラーについては、@aws-sdk/client-dynamodb
パッケージによってエクスポートされたエラータイプで instanceof
演算子を利用して、エラー処理を効率的に管理できます。
こうしたエラーは、再試行回数をすべて使い切って初めて露呈する点に注意が重要です。エラーが再試行され、最終的に呼び出しが成功した場合、コードの観点ではエラーは発生せず、レイテンシーが若干長引いただけになります。再試行は、スロットルリクエストやエラーリクエストなど、失敗したリクエストとして Amazon CloudWatch のグラフに表示されます。クライアントは再試行回数が最大回数に達すると、例外を生成します。つまり、クライアントはそれ以上再試行しないということがわかります。
以下のスニペットは、エラーをキャッチし、返されたエラーのタイプに基づいてアクションを実行します。
import {
ResourceNotFoundException
ProvisionedThroughputExceededException,
DynamoDBServiceException,
} from "@aws-sdk/client-dynamodb";
try {
await client.send(someCommand);
} catch (e) {
if (e instanceof ResourceNotFoundException) {
// Handle ResourceNotFoundException
} else if (e instanceof ProvisionedThroughputExceededException) {
// Handle ProvisionedThroughputExceededException
} else if (e instanceof DynamoDBServiceException) {
// Handle DynamoDBServiceException
} else {
// Other errors such as those from the SDK
if (e.name === "TimeoutError") {
// Handle SDK TimeoutError.
} else {
// Handle other errors.
}
}
}
一般的なエラー文字列については、「DynamoDB デベロッパーガイド」の「DynamoDB でのエラー処理」を参照してください。特定の API コールで発生する可能性のある具体的なエラーについては、Query API のドキュメントなど、該当する API コールのドキュメントに記載されています。
エラーのメタデータには、エラーによって異なりますが、追加のプロパティが含まれます。 TimeoutError
の場合、以下に示すように、試行回数と totalRetryDelay
がメタデータに含まれます。
{
"name": "TimeoutError",
"$metadata": {
"attempts": 3,
"totalRetryDelay": 199
}
}
独自の再試行ポリシーを管理する場合は、スロットルとエラーを区別しておきましょう。
-
スロットル (
ProvisionedThroughputExceededException
またはThrottlingException
で示される) は、DynamoDB テーブルまたはパーティションの読み取り容量または書き込み容量を超過したことを通知する正常なサービスを示します。1 ミリ秒が経過するごとに、読み取りまたは書き込みの容量が少しずつ増えるため、すぐに (50 ミリ秒ごとなど) 再試行して、新しく解放された容量へのアクセスを試みることができます。スロットルについては、エクスポネンシャルバックオフは特に必要ありません。軽量で DynamoDB が返しやすく、リクエストごとの課金も発生しないためです。エクスポネンシャルバックオフでは、すでに最長の待機時間が過ぎたクライアントスレッドの遅延をさらに長くしていくため、統計的に p50 と p99 が外側に広がります。
-
エラー (
InternalServerError
やServiceUnavailable
などで示される) が発生した場合は、サービス (テーブル全体、または読み取り/書き込み先のパーティションなど) に一時的な問題があります。エラーについては、再試行する前に比較的長く (250 ミリ秒や 500 ミリ秒など) 一時停止したり、ジッターを追加して再試行をずらしたりできます。
ログ記録
ログ記録を有効にすると、SDK による処理内容を詳しく把握できます。以下の例に示すように、DynamoDBClient
にパラメータを設定できます。ステータスコードや消費容量などのメタデータを含む詳細なログ情報がコンソールに表示されます。コードをターミナルウィンドウでローカルに実行した場合は、そのウィンドウにログが表示されます。AWS Lambda でコードを実行した場合、Amazon CloudWatch Logs が設定されていれば、コンソール出力がそのログに書き込まれます。
const client = new DynamoDBClient({
logger: console
});
また、内部 SDK アクティビティにフックして、特定のイベントが発生したときにカスタムのログ記録を実行することもできます。以下の例では、クライアントの middlewareStack
を使用して、各リクエストを SDK から送信された時点でインターセプトし、発生時にログに記録します。
const client = new DynamoDBClient({});
client.middlewareStack.add(
(next) => async (args) => {
console.log("Sending request from AWS SDK", { request: args.request });
return next(args);
},
{
step: "build",
name: "log-ddb-calls",
}
);
MiddlewareStack
は、SDK の動作を観察して制御するための強力なフックを提供します。詳細については、ブログ「Introducing Middleware Stack in Modular AWS SDK for JavaScript
考慮事項
プロジェクトに AWS SDK for JavaScript を実装する際には、さらに以下の点を考慮する必要があります。
- モジュールシステム
-
SDK は CommonJS と ES (ECMAScript) の 2 つのモジュールシステムに対応しています。CommonJS は
require
関数を使用し、ES はimport
キーワードを使用します。-
CommonJS –
const { DynamoDBClient, PutItemCommand } = require("@aws-sdk/client-dynamodb");
-
ES (ECMAScript –
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
プロジェクトタイプは package.json ファイルの type セクションで指定され、使用するモジュールシステムを決定します。デフォルトは CommonJS です。
"type": "module"
を使用して、ES プロジェクトを指定します。CommonJS パッケージ形式を使用する既存の Node.JS プロジェクトがある場合でも、関数ファイルに .mjs 拡張子の付いた名前を指定することで、より新しい SDK V3 Import 構文を使用して関数を追加できます。そうすることで、コードファイルを ES (ECMAScript) として扱うことができます。 -
- 非同期オペレーション
-
コールバックとプロミスを使用して DynamoDB オペレーションの結果を処理するサンプルコードが多数あります。最近の JavaScript では、そのように複雑な処理は必要なく、デベロッパーはもっと簡潔で読みやすい async/await 構文を活用して非同期オペレーションを実行できます。
- ウェブブラウザランタイム
-
React または React Native を使用してビルドしているウェブ開発者やモバイル開発者は、各自のプロジェクトで SDK for JavaScript を使用できます。SDK の旧バージョン V2 では、ウェブ開発者は https://sdk.amazonaws.com/js/ でホストされている SDK イメージを参照して、SDK 全体をブラウザにロードする必要がありました。
V3 では、必要な V3 クライアントモジュールと必要なすべての JavaScript 関数を Webpack を使用して単一の JavaScript ファイルにバンドルし、HTML ページの
<head>
のスクリプトタグに追加できます。詳しくは、SDK ドキュメントの「Getting started in a browser script」セクションで説明しています。 - DAX データプレーンオペレーション
-
SDK for JavaScript V3 は、現時点では Amazon DynamoDB Streams Accelerator (DAX) データプレーンオペレーションには対応していません。DAX サポートが必要な場合は、DAX データプレーン操作に対応している SDK for JavaScript V2 の使用を検討してください。