まくろぐ

Node.js で Amazon DynamoDB を操作する

更新:
作成:

ここでは、Node.js 用の AWS SDK を使って Amazon DynamoDB を操作する方法を説明します。 TypeScript の基本的な環境構築 は終わっているものとします。

DynamoDB 用の Node.js SDK をインストールする

AWS SDK version 3 の DynamoDB 用パッケージをインストールするには次のようにします。

$ npm install @aws-sdk/client-dynamodb --save

これで、TypeScript コードから次のようにパッケージ内のクラスをインポートできるようになります。

main.ts
import {DynamoDBClient} from '@aws-sdk/client-dynamodb';

DynamoDBClient インスタンスの生成

DynamoDB の API を呼び出すには、まずは DynamoDBClient オブジェクトを生成する必要があります。 コンストラクタには、DynamoDBClientConfig オブジェクトを渡すようになっており、これで接続先のリージョンやエンドポイント URL などを設定できるようになっています。 例えば、DynamoDB Local によるテスト用サーバ (localhost:8000) に接続するには次のようにします。

main.ts
import {
  DynamoDBClient,
  DynamoDBClientConfig,
} from '@aws-sdk/client-dynamodb';

const CONFIG: DynamoDBClientConfig = {
    endpoint: 'http://localhost:8000'
};

const dbclient = new DynamoDBClient(CONFIG);

古い AWS SDK (version 2) では、接続設定などを AWS.config を使って行っていました。

AWS.config.update({
  region: 'us-west-2',
  endpoint: 'http://localhost:8000',
  accessKeyId: 'fakeMyKeyId',
  secretAccessKey: 'fakeSecretAccessKey'
});

この方法は、プログラムの途中で設定を変更することになるため、全体の振る舞いを追いにくくなるという問題がありました。 AWS SDK version 3 では、各クライアントクラスのコンストラクタでコンフィグレーション情報を渡す設計になっています。

テーブルを作成する (CreateTableCommand)

DynamoDB のテーブルを作成するには、DynamoDBClient#send メソッドで、CreateTableCommand を送ります。

次の例では、Games という名前のテーブルを作成しています。 DynamoDB は NoSQL なので、基本的に属性を前もって定義しておく必要はありませんが、プライマリーキー(パーティションキー、およびソートキー)だけはテーブル作成時に指定する必要があります。 Games テーブルでは、次のようなプライマリーキーを定義しています。

  • Hardware: 文字列型 (S) のパーティションキー (HASH)
  • GameId: 文字列型 (S) のソートキー (RANGE)
create_table.ts
import {CreateTableCommand, DynamoDBClient} from '@aws-sdk/client-dynamodb';

const dbclient = new DynamoDBClient({
  endpoint: 'http://localhost:8000'
});

// テーブルを作成する
async function createTable() {
  try {
    const command = new CreateTableCommand({
      TableName: 'Games',  // テーブル名
      KeySchema: [
        { AttributeName: "Hardware", KeyType: "HASH" },  // パーティションキー
        { AttributeName: "GameId", KeyType: "RANGE" },  // ソートキー
      ],
      AttributeDefinitions: [
        { AttributeName: 'Hardware', AttributeType: 'S' },  // 文字列属性
        { AttributeName: 'GameId', AttributeType: 'S' },  // 文字列属性
      ],
      ProvisionedThroughput: {
        ReadCapacityUnits: 1,
        WriteCapacityUnits: 1,
      },
      StreamSpecification: {
        StreamEnabled: false,
      },
    });
    const output = await dbclient.send(command);
    console.log('SUCCESS: Table created:', output);
  } catch (err) {
    console.error('ERROR:', err);
  }
}

createTable();

DynamoDBClient#send メソッドの戻り値は Promise 型になるので、コマンドの結果を参照する場合は、上記のように await で待機する必要があります。

SUCCESS: Table created: {
  '$metadata': {
    httpStatusCode: 200,
    requestId: '0074dc26-ba2e-4eab-b432-14874d58fd91',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  },
  TableDescription: {
    ArchivalSummary: undefined,
    AttributeDefinitions: [ [Object], [Object] ],
    BillingModeSummary: undefined,
    CreationDateTime: 2021-02-27T14:44:46.724Z,
    GlobalSecondaryIndexes: undefined,
    GlobalTableVersion: undefined,
    ItemCount: 0,
    KeySchema: [ [Object], [Object] ],
    LatestStreamArn: undefined,
    LatestStreamLabel: undefined,
    LocalSecondaryIndexes: undefined,
    ProvisionedThroughput: {
      LastDecreaseDateTime: 1970-01-01T00:00:00.000Z,
      LastIncreaseDateTime: 1970-01-01T00:00:00.000Z,
      NumberOfDecreasesToday: 0,
      ReadCapacityUnits: 1,
      WriteCapacityUnits: 1
    },
    Replicas: undefined,
    RestoreSummary: undefined,
    SSEDescription: undefined,
    StreamSpecification: undefined,
    TableArn: 'arn:aws:dynamodb:ddblocal:000000000000:table/Games',
    TableId: undefined,
    TableName: 'Games',
    TableSizeBytes: 0,
    TableStatus: 'ACTIVE'
  }
}

例外

すでに同名のテーブルが存在するときに CreateTableCommand を送ると、ResourceInUseException という例外が発生します。

  • err.code === 'ResourceInUseException' … Table already exists

テーブルを削除する (DeleteTableCommand)

DynamoDB から既存のテーブルを削除するには、DynamoDBClient.sendDeleteTableCommand を送ります。 パラメーターはテーブル名 (TableName) だけでいいので簡単です(もちろんテーブルの削除は慎重に行わなければいけませんが)。

delete_table.ts
import {DeleteTableCommand, DynamoDBClient} from '@aws-sdk/client-dynamodb';

const dbclient = new DynamoDBClient({
  endpoint: 'http://localhost:8000'
});

// テーブルを削除する
async function deleteTable() {
  try {
    const command = new DeleteTableCommand({
      TableName: 'Games',
    });
    const output = await dbclient.send(command);
    console.log('SUCCESS: Table deleted:', output);
  } catch (err) {
    console.error('ERROR:', err);
  }
};

deleteTable();
SUCCESS: Table deleted: {
  '$metadata': {
    httpStatusCode: 200,
    requestId: '99d6402d-5de7-4b3f-91b6-c11e97aae417',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  },
  TableDescription: {
    ArchivalSummary: undefined,
    AttributeDefinitions: [ [Object], [Object] ],
    BillingModeSummary: undefined,
    CreationDateTime: 2021-02-27T14:16:30.122Z,
    GlobalSecondaryIndexes: undefined,
    GlobalTableVersion: undefined,
    ItemCount: 0,
    KeySchema: [ [Object], [Object] ],
    LatestStreamArn: undefined,
    LatestStreamLabel: undefined,
    LocalSecondaryIndexes: undefined,
    ProvisionedThroughput: {
      LastDecreaseDateTime: 1970-01-01T00:00:00.000Z,
      LastIncreaseDateTime: 1970-01-01T00:00:00.000Z,
      NumberOfDecreasesToday: 0,
      ReadCapacityUnits: 1,
      WriteCapacityUnits: 1
    },
    Replicas: undefined,
    RestoreSummary: undefined,
    SSEDescription: undefined,
    StreamSpecification: undefined,
    TableArn: 'arn:aws:dynamodb:ddblocal:000000000000:table/Games',
    TableId: undefined,
    TableName: 'Games',
    TableSizeBytes: 0,
    TableStatus: 'ACTIVE'
  }
}

例外

削除しようとしたテーブルが存在しない場合や、使用中の場合は例外が発生します。

  • err.code === 'ResourceNotFoundException' … Table not found
  • err.code === 'ResourceInUseException' … Table in use

テーブルの一覧を取得する (ListTablesCommand)

DynamoDB に存在するテーブルの一覧を取得するには、DynamoDBClient#sendListTablesCommand を送ります。 パラメータは空っぽの ListTablesInput オブジェクトを渡しておけば OK です。 戻り値の ListTablesCommandOutput オブジェクトの TableNames プロパティに、テーブル名のリストが格納されています。

list_tables.ts
import {DynamoDBClient, ListTablesCommand} from '@aws-sdk/client-dynamodb';

const dbclient = new DynamoDBClient({
  endpoint: 'http://localhost:8000'
});

// テーブルの一覧を取得する
async function listTables() {
  try {
    const command = new ListTablesCommand({});
    const output = await dbclient.send(command);
    if (output.TableNames) {
      console.log(output.TableNames.join(', '));
    }
    console.log(output);
  } catch (err) {
    console.error('ERROR', err);
  }
}

listTables();
Games
{
  '$metadata': {
    httpStatusCode: 200,
    requestId: 'f80bd8f7-788f-4f17-b81e-dd6e2f529ebf',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  },
  LastEvaluatedTableName: undefined,
  TableNames: [ 'Games' ]
}

テーブルの詳細情報を取得する (DescribeTableCommand)

指定したテーブルの詳細情報(スキーマなど)を調べるには、DynamoDBClient#sendDescribeTableCommand を送ります。 パラメータには TableName を指定します。

describe_tables.ts
import {DescribeTableCommand, DynamoDBClient} from '@aws-sdk/client-dynamodb';

const dbclient = new DynamoDBClient({
  endpoint: 'http://localhost:8000'
});

// テーブルの詳細情報を取得する
async function describeTable() {
  try {
    const command = new DescribeTableCommand({
      TableName: 'Games'
    });
    const output = await dbclient.send(command);
    console.log('SUCCESS (describe table):', JSON.stringify(output, null, 2));
  } catch (err) {
    console.error('ERROR:', err);
  }
}

describeTable();
SUCCESS (describe table): {
  "$metadata": {
    "httpStatusCode": 200,
    "requestId": "b700a435-c7ee-4231-a010-6e05dbda9ab6",
    "attempts": 1,
    "totalRetryDelay": 0
  },
  "Table": {
    "AttributeDefinitions": [
      {
        "AttributeName": "Hardware",
        "AttributeType": "S"
      },
      {
        "AttributeName": "GameId",
        "AttributeType": "S"
      }
    ],
    "CreationDateTime": "2021-02-27T15:07:34.953Z",
    "ItemCount": 0,
    "KeySchema": [
      {
        "AttributeName": "Hardware",
        "KeyType": "HASH"
      },
      {
        "AttributeName": "GameId",
        "KeyType": "RANGE"
      }
    ],
    "ProvisionedThroughput": {
      "LastDecreaseDateTime": "1970-01-01T00:00:00.000Z",
      "LastIncreaseDateTime": "1970-01-01T00:00:00.000Z",
      "NumberOfDecreasesToday": 0,
      "ReadCapacityUnits": 1,
      "WriteCapacityUnits": 1
    },
    "TableArn": "arn:aws:dynamodb:ddblocal:000000000000:table/Games",
    "TableName": "Games",
    "TableSizeBytes": 0,
    "TableStatus": "ACTIVE"
  }
}

アイテムを追加する (PutItemCommand)

テーブルに新しいアイテムを追加したいとき(あるいは既存のアイテムの属性値を上書きしたいとき)は、DynamoDBClient#send メソッドで PutItemCommand を送ります。

追加するアイテムのプライマリキー属性(パーティションキー、およびソートキー)の指定は必須ですが、その他の属性はオプショナルです。 プライマリキーと一致するアイテムがまだ存在しない場合は、新規アイテムの追加になり、プライマリキーと一致するアイテムがすでに存在する場合は、属性値のリプレースになります(このとき、指定しなかった属性値は消えてしまうことに注意してください)。

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

const dbclient = new DynamoDBClient({
  endpoint: 'http://localhost:8000'
});

// テーブルにアイテムを追加する
async function putItem() {
  try {
    const command = new PutItemCommand({
      TableName: 'Games',
      Item: {
        Hardware: { S: 'SNES' },
        GameId: { S: '1990-SuperMarioWorld' },
        Title: { S: 'Super Mario World' },
        Players: { N: '2' },
        Genre: { S: 'ACT' },
      },
    });
    const output = await dbclient.send(command);
    console.log('SUCCESS (put item):', output);
  } catch (err) {
    console.log('ERROR:', err);
  }
}

putItem();
SUCCESS (put item): {
  '$metadata': {
    httpStatusCode: 200,
    requestId: 'e230fba2-732b-4f44-a0f2-ddca742f63fe',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  },
  Attributes: undefined,
  ConsumedCapacity: undefined,
  ItemCollectionMetrics: undefined
}

上書きされる前のアイテムの内容を取得する

PutItemCommand コマンドを送信したときに、プライマリキーと一致するアイテムがすでに存在する場合は、新しく指定した属性値でその内容が置き換えられます。 このとき、もとのアイテムがどのような属性を持っていたかを調べたいときは、PutItemCommand を生成するときに、ReturnValues プロパティを指定します。 コマンド送信の結果の Attributes プロパティでもとの属性値を参照できます。

async function putItem() {
  try {
    const command = new PutItemCommand({
      TableName: 'Games',
      Item: {
        Hardware: { S: 'SNES' },
        GameId: { S: '1990-SuperMarioWorld' },
        Title: { S: 'XXXXX' }, // 値を適当に変えてみる
        Players: { N: '2' },
        Genre: { S: 'ACT' },
      },
      ReturnValues: 'ALL_OLD',  // もとの属性値を取得
    });
    const output = await dbclient.send(command);
    console.log('SUCCESS (put item):', output.Attributes);
  } catch (err) {
    console.log('ERROR:', err);
  }
}
実行結果
SUCCESS (put item): {
  Title: { S: 'Super Mario World' },
  Hardware: { S: 'SNES' },
  Genre: { S: 'ACT' },
  GameId: { S: '1990-SuperMarioWorld' },
  Players: { N: '2' }
}

すでにアイテムが存在する場合に無視する

PutItemCommand コマンドはデフォルトではアイテムの内容を上書きしますが、この振る舞いを抑制したいときは、コマンドのパラメータに次のように ConditionExpression を指定します。 このようにすると、プライマリキーが一致するアイテムがすでに存在する場合に ConditionalCheckFailedException 例外が発生するようになります。

const command = new PutItemCommand({
  TableName: 'Games',
  Item: {
    Hardware: { S: 'SNES' },
    GameId: { S: '1990-SuperMarioWorld' },
    Title: { S: 'XXXXX' }, // 値を適当に変えてみる
    Players: { N: '2' },
    Genre: { S: 'ACT' },
  },
  ConditionExpression: 'attribute_not_exists(Hardware)',
});

アイテムを削除する (DeleteItemCommand)

DynamoDB のテーブルからアイテムを削除するには、DynamoDBClient#send メソッドで DeleteItemCommand を送ります。 コマンドのパラメータには、テーブル名 (Table) と、削除対象のアイテムを特定するためのプライマリキー情報 (Key) を渡す必要があります。 複合プライマリキーを使用している場合は、パーティションキーとソートキーの両方を指定する必要があります。

delete_item.ts
import {DeleteItemCommand, DynamoDBClient} from '@aws-sdk/client-dynamodb';

const dbclient = new DynamoDBClient({
  endpoint: 'http://localhost:8000'
});

// アイテムを削除する
async function deleteItem() {
  try {
    const command = new DeleteItemCommand({
      TableName: 'Games',
      Key: {
        Hardware: {S: 'SNES'},
        GameId: {S: '1990-SuperMarioWorld'},
      }
    });
    const output = await dbclient.send(command);
    console.log('SUCCESS (delete item):', output);
  } catch (err) {
    console.log('ERROR:', err);
  }
}

deleteItem();
SUCCESS (delete item): {
  '$metadata': {
    httpStatusCode: 200,
    requestId: 'cdfcb727-b39e-413a-a643-c3f6a11b95c0',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  },
  Attributes: undefined,
  ConsumedCapacity: undefined,
  ItemCollectionMetrics: undefined
}

指定したプライマリキーに一致するアイテムが存在しない場合でも、特に例外が発生するということはないようです。

アイテムを取得する (GetItemCommand)

DynamoDB のテーブルから既存のアイテムを 1 つ取得するときは、DynamoDBClient#send メソッドで GetItemCommand を送ります。 パラメータには、テーブル名 (Table) と、アイテムを特定するためのプライマリキー情報 (Key) を指定します。

get_item.ts
import {DynamoDBClient, GetItemCommand} from '@aws-sdk/client-dynamodb';

const dbclient = new DynamoDBClient({
  endpoint: 'http://localhost:8000'
});

// アイテムを取得する
async function getItem() {
  try {
    const command = new GetItemCommand({
      TableName: 'Games',
      Key: {
        Hardware: {S: 'SNES'},
        GameId: {S: '1990-SuperMarioWorld'},
      }
    });
    const output = await dbclient.send(command);
    console.log('SUCCESS (get item):', output);
  } catch (err) {
    console.log('ERROR:', err);
  }
}

getItem();
SUCCESS (get item): {
  '$metadata': {
    httpStatusCode: 200,
    requestId: '162f4a24-8b5c-4174-a83d-c581df44ca80',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  },
  ConsumedCapacity: undefined,
  Item: {
    Title: { S: 'Super Mario World' },
    Hardware: { S: 'SNES' },
    Genre: { S: 'ACT' },
    GameId: { S: '1990-SuperMarioWorld' },
    Players: { N: '2' }
  }
}

指定したプライマリキーに一致するアイテムが見つからないときは、Item プロパティの値が undefined になります。

アイテムの属性値を部分的に更新する (UpdateItemCommand)

既存のアイテムの内容(属性値)を部分的に更新したいときは、DynamoDBClient#send メソッドで UpdateItemCommand を送ります。 PutItemCommand でも既存のアイテムを更新できますが、PutItemCommand は完全に上書きのため、すべての属性値を指定し直さないといけないません。 一方、UpdateItemCommand を使用すると、指定した属性のみを部分的に更新できます。 なお、どちらも指定したアイテム自体が存在しない場合にアイテムが生成されるのは同様です。

UpdateItemCommand で属性値を更新するときのパラメータの指定方法は若干複雑で、プレースホルダを使って新しい属性名 (#xxx) と属性値 (:xxx) を指定する必要があります。 次の例では、既存のアイテムの Title 属性の値を更新し、さらに新しい属性 Maker を追加しています。

update_item.ts
import {DynamoDBClient, UpdateItemCommand} from '@aws-sdk/client-dynamodb';

const dbclient = new DynamoDBClient({
  endpoint: 'http://localhost:8000'
});

// アイテムの属性値を部分的に更新する
async function updateItem() {
  try {
    const command = new UpdateItemCommand({
      TableName: 'Games',
      Key: {
        Hardware: {S: 'SNES'},
        GameId: {S: '1990-SuperMarioWorld'},
      },
      UpdateExpression: 'set Title = :x, #a = :y',
      ExpressionAttributeNames: {
        '#a': 'Maker',
      },
      ExpressionAttributeValues: {
        ':x': {S: 'Mario 4'},
        ':y': {S: 'Nintendo'}
      },
    });
    const output = await dbclient.send(command);
    console.log('SUCCESS (update item):', output);
  } catch (err) {
    console.log('ERROR:', err);
  }
}

updateItem();
SUCCESS (update item): {
  '$metadata': {
    httpStatusCode: 200,
    requestId: 'b2bae6de-ceed-42c6-8946-455b75d2472e',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  },
  Attributes: undefined,
  ConsumedCapacity: undefined,
  ItemCollectionMetrics: undefined
}

関連記事

まくろぐ
サイトマップまくへのメッセージ