DynamoDBチュートリアル(TypeScript)

DynamoDB

DynamoDBの操作方法についてまとめました。

公式の方はpythonで説明されています。TypeScriptで操作できるようにしたいのでまとめました。

Module 1: Application Background
Module 1 of the course Create and Manage a Nonrelational Database is titled: Application Background .To get started, you will explore the tutorial prerequisites...

環境構築

npm i @aws-sdk/client-dynamodb

typescriptの設定をします。

npm i -D typescript @types/node ts-node

npx tsc --init
touch sample.ts

import {
  DynamoDBClient,
  DynamoDBClientConfig,
} from "@aws-sdk/client-dynamodb";

const config: DynamoDBClientConfig = {
  // 設定あれば
}
const client = new DynamoDBClient(config);
console.log("hello from DynamoDB");

npx ts-node sample.ts

これでコンソールに「hello from DynamoDB」とログが出ればOKです。

(npm scriptsに登録してもいいですね!)

DynamoDBのデータ型

次のようなデータ型があります。

S」や「N」というように英字で指定します。

DynamoDB データ型記述子の一覧を次に示します。

  • S — 文字列
  • N — 数値
  • B — バイナリ
  • BOOL — ブール
  • NULL — Null
  • M — マップ
  • L — リスト
  • SS — 文字列セット
  • NS — 数値セット
  • BS — バイナリセット
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypeDescriptors

パーティションキーとソートキー

これらは重要な概念なのでまとめます。

  • パーティションキー
    • テーブルのデータを物理的に分割し、複数のパーティションに分散する。
  • ソートキー
    • テーブルのデータを並べ替えるためのキー。

パーティションキーで箱に分割し、ソートキーで箱の中身を並び替えるイメージです。

今回扱うデータ

果物屋の商品在庫管理をテーマにします。

属性名データ型説明
FruitID文字列果物の一意の識別子
Name文字列果物の名前
Price数値果物の価格
Stock数値在庫数
ExpirationDate文字列果物の賞味期限

これらをコマンドラインからCRUDできるように、TypeScriptで実装していきます。

テーブル作成

createTable.tsを作成します。

CreateTableCommandを使用します。

import { CreateTableCommand, DynamoDBClient } from "@aws-sdk/client-dynamodb";

async function createTable(client: DynamoDBClient) {
  try {
    const command = new CreateTableCommand({
      TableName: "Fruits",
      KeySchema: [
        { AttributeName: "FruitID", KeyType: "HASH" },
        { AttributeName: "Name", KeyType: "RANGE" },
      ],
      AttributeDefinitions: [
        { AttributeName: "FruitID", AttributeType: "S" },
        { AttributeName: "Name", AttributeType: "S" },
      ],
      ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 },
    });
    const response = await client.send(command);
    console.log("Table created successfully!", response);
  } catch (err) {
    console.error("Error creating table:", err);
  }
}
export default createTable;

sample.tsから呼び出して実行します。

import {
  DynamoDBClient,
  DynamoDBClientConfig,
} from "@aws-sdk/client-dynamodb";
import createTable from "./createTable";

const config: DynamoDBClientConfig = {
  // 設定あれば
}
const client = new DynamoDBClient(config);

createTable(client);

FruitIDとNameをパーティションキーとソートキーに指定しました。

コマンドを実行し、テーブルが作成できればOKです。

データ投入

insertItems.tsを作成します。

PutItemCommandを使用します。

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

const fruits = [
  { 
    FruitID: { S: "1" }, 
    Name: { S: "りんご" }, 
    Price: { N: "100" }, 
    Stock: { N: "150" }, 
    ExpirationDate: { S: "2024-03-20" } 
  },
  { 
    FruitID: { S: "2" }, 
    Name: { S: "バナナ" }, 
    Price: { N: "230" }, 
    Stock: { N: "180" }, 
    ExpirationDate: { S: "2024-03-25" } 
  },
  { 
    FruitID: { S: "3" }, 
    Name: { S: "オレンジ" }, 
    Price: { N: "150" }, 
    Stock: { N: "200" }, 
    ExpirationDate: { S: "2024-03-18" } 
  },
  { 
    FruitID: { S: "4" }, 
    Name: { S: "りんご" }, 
    Price: { N: "120" }, 
    Stock: { N: "120" }, 
    ExpirationDate: { S: "2024-03-22" } 
  },
  { 
    FruitID: { S: "5" }, 
    Name: { S: "バナナ" }, 
    Price: { N: "180" }, 
    Stock: { N: "160" }, 
    ExpirationDate: { S: "2024-03-27" } 
  },
  { 
    FruitID: { S: "6" }, 
    Name: { S: "オレンジ" }, 
    Price: { N: "200" }, 
    Stock: { N: "170" }, 
    ExpirationDate: { S: "2024-03-16" } 
  },
  { 
    FruitID: { S: "7" }, 
    Name: { S: "りんご" }, 
    Price: { N: "110" }, 
    Stock: { N: "140" }, 
    ExpirationDate: { S: "2024-03-24" } 
  },
  { 
    FruitID: { S: "8" }, 
    Name: { S: "バナナ" }, 
    Price: { N: "250" }, 
    Stock: { N: "190" }, 
    ExpirationDate: { S: "2024-03-30" } 
  },
  { 
    FruitID: { S: "9" }, 
    Name: { S: "オレンジ" }, 
    Price: { N: "170" }, 
    Stock: { N: "180" }, 
    ExpirationDate: { S: "2024-03-17" } 
  },
  { 
    FruitID: { S: "10" }, 
    Name: { S: "りんご" }, 
    Price: { N: "130" }, 
    Stock: { N: "160" }, 
    ExpirationDate: { S: "2024-03-21" } 
  },
  // 同じ名前の果物を追加
  { 
    FruitID: { S: "11" }, 
    Name: { S: "バナナ" }, 
    Price: { N: "200" }, 
    Stock: { N: "150" }, 
    ExpirationDate: { S: "2024-03-26" } 
  },
  { 
    FruitID: { S: "12" }, 
    Name: { S: "オレンジ" }, 
    Price: { N: "180" }, 
    Stock: { N: "190" }, 
    ExpirationDate: { S: "2024-03-15" } 
  },
  { 
    FruitID: { S: "13" }, 
    Name: { S: "りんご" }, 
    Price: { N: "140" }, 
    Stock: { N: "170" }, 
    ExpirationDate: { S: "2024-03-23" } 
  },
  // 他の果物も同様に追加
  { 
    FruitID: { S: "14" }, 
    Name: { S: "バナナ" }, 
    Price: { N: "210" }, 
    Stock: { N: "170" }, 
    ExpirationDate: { S: "2024-03-28" } 
  },
  { 
    FruitID: { S: "15" }, 
    Name: { S: "オレンジ" }, 
    Price: { N: "190" }, 
    Stock: { N: "180" }, 
    ExpirationDate: { S: "2024-03-19" } 
  },
  { 
    FruitID: { S: "16" }, 
    Name: { S: "りんご" }, 
    Price: { N: "150" }, 
    Stock: { N: "150" }, 
    ExpirationDate: { S: "2024-03-25" } 
  },
  { 
    FruitID: { S: "17" }, 
    Name: { S: "バナナ" }, 
    Price: { N: "220" }, 
    Stock: { N: "160" }, 
    ExpirationDate: { S: "2024-03-29" } 
  },
  { 
    FruitID: { S: "18" }, 
    Name: { S: "オレンジ" }, 
    Price: { N: "160" }, 
    Stock: { N: "190" }, 
    ExpirationDate: { S: "2024-03-20" } 
  },
  { 
    FruitID: { S: "19" }, 
    Name: { S: "りんご" }, 
    Price: { N: "170" }, 
    Stock: { N: "140" }, 
    ExpirationDate: { S: "2024-03-26" } 
  },
  { 
    FruitID: { S: "20" }, 
    Name: { S: "バナナ" }, 
    Price: { N: "240" }, 
    Stock: { N: "180" }, 
    ExpirationDate: { S: "2024-03-31" } 
  }
];

async function insertItems(client: DynamoDBClient) {
  try {
    for (const fruit of fruits) {      
      const command = new PutItemCommand({
        TableName: "Fruits",
        Item: fruit,
      });
      await client.send(command);
    }
    console.log("Items inserted successfully!");
  } catch (err) {
    console.error("Error inserting items:", err);
  }
}
export default insertItems;

sample.tsから呼び出してコマンドを実行します。

import {
  DynamoDBClient,
  DynamoDBClientConfig,
} from "@aws-sdk/client-dynamodb";
import createTable from "./createTable";
import insertItems from "./insertItems";

const config: DynamoDBClientConfig = {
  // 設定あれば
}
const client = new DynamoDBClient(config);

// createTable(client);
insertItems(client);

マネジメントコンソールの「項目を検索」から、投入したデータを見ると

投入できました!

データの取得

insertItems.tsを作成します。

getItemCommandを使用します。

import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";

async function getItem(client: DynamoDBClient) {
  const command = new GetItemCommand({
    TableName: "Fruits",
    Key: {
      FruitID: {
        S: "1",
      },
      Name: {
        S: "りんご",
      },
    },
  });
  const response = await client.send(command);
  console.log(response.Item);
}

export default getItem;

パーティションキーとソートキーのどちらも指定が必要。

呼び出して実行します。

import {
  DynamoDBClient,
  DynamoDBClientConfig,
} from "@aws-sdk/client-dynamodb";
import createTable from "./createTable";
import insertItems from "./insertItems";
import getItem from "./getItem";

const config: DynamoDBClientConfig = {
  // 設定あれば
}
const client = new DynamoDBClient(config);

// createTable(client);
// insertItems(client);
getItem(client);

コンソールに出ればOKです。

データの検索

getItemではパーティションキーとソートキーの指定でしか検索できません。

ソートキーにはNameが指定されているので、金額や在庫などを条件にしたクエリが難しい状態です。

そのために、セカンダリインデックスを使用します。

セカンダリインデックスとは?

GSI(グローバルセカンダリインデックス)、LSI(ローカルセカンダリインデックス)の2種類があります。

  • GSI
    • 異なるパーティションキーとソートキーを持つ。
    • テーブル作成後に作成。
  • LSI
    • ソートキーを作成する。
    • テーブル作成時に作成。

自分はこの2つの理解にハマりました。

パフォーマンスとか意識したいならあらかじめLSIで作成しておく、という認識です。

ここでは、GSIを設定してクエリを書いてきます。

金額を条件にクエリ

「120円より安いりんごを取得」してみましょう。

addSecondaryIndex.tsを作成します。

UpdateTableCommandを使用して、GSIを追加します。

import { DynamoDBClient, UpdateTableCommand } from "@aws-sdk/client-dynamodb";

async function addSecondaryIndex(client: DynamoDBClient) {
  try {
    const command = new UpdateTableCommand({
      TableName: "Fruits",
      AttributeDefinitions: [
        { AttributeName: "Name", AttributeType: "S" },
        { AttributeName: "Price", AttributeType: "N" },
      ],
      GlobalSecondaryIndexUpdates: [
        {
          Create: {
            IndexName: "PriceIndex",
            KeySchema: [
              { AttributeName: "Name", KeyType: "HASH" },
              { AttributeName: "Price", KeyType: "RANGE" },
            ],
            Projection: {
              ProjectionType: "ALL",
            },
            ProvisionedThroughput: {
              ReadCapacityUnits: 5,
              WriteCapacityUnits: 5,
            },
          },
        },
      ],
    });
    const response = await client.send(command);
    console.log("secondaryIndex added successfully!", response);
  } catch (error) {
    console.error("Error adding secondaryIndex:", error);
  }
}
export default addSecondaryIndex;

パーティーションキーは「Name」、ソートキーを「Price」で指定します。

ProjectionType(射影タイプ)の設定は下記の通りです。

  • KEYS_ONLY – インデックス内の各項目は、テーブルパーティションキーとソートキーの値、およびインデックスキーの値のみで構成されます。KEYS_ONLY オプションを指定すると、セカンダリインデックスが最小になります。
  • INCLUDE – KEYS_ONLY の属性に加えて、セカンダリインデックスにその他の非キー属性が含まれるように指定できます。
  • ALL – セカンダリインデックスには、ソーステーブルのすべての属性が含まれます。すべてのテーブルデータがインデックスに複製されるため、ALL の射影にすると、セカンダリインデックスが最大になります。
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/GSI.html#GSI.Projections

コスト最適化の際に考える必要がありますが、ここではALLにしています。

ちなみに、LSIはCreateTableCommandで同様に指定することで作成可能です。

queryItemsWithPriceIndex.tsを作成します。

import { DynamoDBClient, QueryCommand } from "@aws-sdk/client-dynamodb";

async function queryItemsWithPriceIndex(client: DynamoDBClient) {
  const command = new QueryCommand({
    TableName: "Fruits",
    IndexName: "PriceIndex",
    ExpressionAttributeNames: {
      "#fruit_name":"Name" // nameは予約語なので変換する。#で始める
    },
    KeyConditionExpression: "#fruit_name = :fruit_name and Price < :max_price",
    ExpressionAttributeValues: {
      ":fruit_name": { S: "りんご" },
      ":max_price": { N: "200" },
    },
  });
  const response = await client.send(command);
  console.log(response.Items);
}
export default queryItemsWithPriceIndex;

ExpressionAttributeNamesについては下記を参考にしてください。

DynamoDB の式の属性名 - Amazon DynamoDB
Amazon DynamoDB でクエリまたはスキャンする場合は、式の属性名を使用します。これは、式の実際の属性名の代わりに使用されるプレースホルダーです。 式の属性名は、実際の属性名の代わりに Amazon DynamoDB 式で使用するプレースホルダーです。DynamoDB のクエリまたはスキャン時にそれらを使用す...

sample.tsで読み込んで実行すると、

在庫や期限を条件にクエリ

もう少し複雑なクエリも実行してみましょう。

「在庫が200個以上で、有効期限が2024/03/20より前の果物を取得する。」

今回はScanを使用します。

Scanはコストが高く、パフォーマンスが低下する可能性があるのでなるべくインデックスを作成しQueryを使用しましょう!」と言われています。

scanItem.tsを作成します。

import { DynamoDBClient, ScanCommand } from "@aws-sdk/client-dynamodb";

async function scanItems(client: DynamoDBClient) {
  const command = new ScanCommand({
    TableName: "Fruits",
    FilterExpression: "#Stock >= :stock AND #ExpirationDate < :expirationDate",
    ExpressionAttributeNames: {
      "#Stock": "Stock",
      "#ExpirationDate": "ExpirationDate",
    },
    ExpressionAttributeValues: {
      ":stock": { N: "200" },
      ":expirationDate": { S: "2024-03-20" },
    },
  });
  const response = await client.send(command);
  console.log(response.Items);
}
export default scanItems;

書き方はQueryと基本的に同じです。

実行して取得できればOKです。

テーブル削除

最後に、片付けをします。

deleteTable.tsを作成します。

import { DeleteTableCommand, DynamoDBClient } from "@aws-sdk/client-dynamodb";

async function deleteTable(client:DynamoDBClient) {
  try {
    const command = new DeleteTableCommand({
      TableName: 'Fruits',
    })
    const response = await client.send(command);
    console.log('Table deleted successfully!', response);
  } catch (err) {
    console.error('Error deleting table:', err);
  }
}
export default deleteTable;

実行し、テーブルが削除されれば完了です。

おまけ

パーティーションキーの検索条件にイコールしか使えないようで、

ValidationException: Query key condition not supported

のエラーが出て驚きました。

例えばパーティーションキーに名前を指定し、前方一致検索したい、とかはできないです。

DynamoDBはRDBよりも検索に向いていないな、と思いましたね。