🪸

【AWS SDK for JavaScript v3】DynamoDB操作のあれこれをTypeScriptで理解する

2024/04/15に公開

この記事で解決すること

SDK for JavaScript v3を用いてDynamoDBを操作する時に使用するライブラリの違い

  • @aws-sdk/client-dynamodbDynamoDBClient
  • @aws-sdk/lib-dynamodbDynamoDBDocumentClient

やりたいこと

SDK for JavaScript v3を用いてDynamoDBにデータを挿入するLambda関数コードを書きます。

環境

Lambda Node.js 20
APIGateway, DynamoDB
(AWS SAMによるデプロイ、APIGatewayコンソール上からテスト)

AWS SDK for JavaScript v3でのDynamoDB操作について

AWS SDK for JavaScript v3では、操作したい各リソースごとにライブラリをインストールして使用します。DynamoDB操作の場合、@aws-sdk/client-dynamodbもしくは@aws-sdk/lib-dynamodbのどちらかをインストールします。

npm install @aws-sdk/client-dynamodb

または

npm install @aws-sdk/lib-dynamodb

ここで謎

選択肢が二つ。どっちでも良さそうに見える。
どう違うの?どっちを使えばいいの??

どう違うの?(ざっくり)

@aws-sdk/client-dynamodb

SDKにおいて、操作可能な各サービスのクライアントライブラリのパッケージ名称は@aws-sdk/client-xxxxxで始まっています。
https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/dynamodb/

@aws-sdk/lib-dynamodb

一方で、@aws-sdk/lib-xxxxxという名称のパッケージ(ライブラリ)があるサービスはDynamoDBとS3のみです。(2024/04/15時点)
例えば@aws-sdk/lib-dynamodbについてはドキュメントに以下のような記載があります。
https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-lib-dynamodb/

The document client simplifies working with items in Amazon DynamoDB by abstracting away the notion of attribute values. This abstraction annotates native JavaScript types supplied as input parameters, as well as converts annotated response data to native JavaScript types.

@aws-sdk/client-dynamodbと異なり@aws-sdk/lib-dynamodbはDynamoDBの属性値の概念を抽象化することで操作を簡素化したものだそうです。
DynamoDBが返すアイテムのデータ型はAttributeValueですが、ドキュメントクライアントにより JavaScriptの型を使用してリクエスト・レスポンスを扱うことができます。
ドキュメントクライアントはDynamoDBClientをラップしたものと言えるでしょう。
分かりづらいので、具体的なコードを見てみましょう。

コードの違いを見る

やること

APIGatewayとLambdaでAPIを実装し、idをPKとするシンプルなDynamoDBテーブルに以下のアイテムをputします。

id name age
1 John 20

テーブルとAPIは作成済みであることを前提とし、以降はLambda関数のコードについてのみ説明します。

@aws-sdk/client-dynamodbの場合

import { DynamoDBClient, PutItemCommandInput, PutItemCommand } from '@aws-sdk/client-dynamodb';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

const dynamodbClient = new DynamoDBClient({
    region: 'ap-northeast-1',
});

export const lambdaHandler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
    const params: PutItemCommandInput = {
        TableName: 'test-table',
        Item: {
            id: { S: '1' },
            name: { S: 'John' },
            age: { N: '20' },
        },
    };
    const command = new PutItemCommand(params);
    try {
        await dynamodbClient.send(command);
        return {
            statusCode: 200,
            body: 'success',
        };
    } catch (err) {
        return {
            statusCode: 500,
            body: JSON.stringify({
                message: 'some error happened',
            }),
        };
    }
};

@aws-sdk/lib-dynamodbの場合

import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand, PutCommandInput } from '@aws-sdk/lib-dynamodb';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

const dynamodbClient = new DynamoDBClient({
    region: 'ap-northeast-1',
});

// ここで、↑で初期化したDynamoDBClientを用いてDynamoDBDocumentClientを初期化
const docClient = DynamoDBDocumentClient.from(dynamodbClient);

export const lambdaHandler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
    const params: PutCommandInput = {
        TableName: 'test-table',
        Item: {
            id: '1', // ここが違う!
            name: 'John', // ここが違う!
            age: 20, // ここが違う!
        },
    };
    const command = new PutCommand(params);
    try {
        await docClient.send(command);
        return {
            statusCode: 200,
            body: 'success',
        };
    } catch (err) {
        return {
            statusCode: 500,
            body: JSON.stringify({
                message: 'some error happened',
            }),
        };
    }
};

何が違う?

  • @aws-sdk/lib-dynamodbを使うと@aws-sdk/client-dynamodbDynamoDBDocumentを用いて初期化した DynamoDBDocumentClientを使用することが、まず一つ目の違いです。

  • そして大きく異なるのは、上記コードのparamsの書き方です。
    @aws-sdk/lib-dynamodbDynamoDBDocumentClientでは、putするItemを単純なkeyとvalueで定義しています。
    これを@aws-sdk/client-dynamodbDynamoDBClientで同じように書いてみましょう。(id: { S: '1' }id: '1'とする)

client-dynamodbのみでlib-dynamodbの書き方をする
import { DynamoDBClient, PutItemCommandInput, PutItemCommand } from '@aws-sdk/client-dynamodb';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

const dynamodbClient = new DynamoDBClient({
    region: 'ap-northeast-1',
});

export const lambdaHandler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
    const params: PutItemCommandInput = {
        TableName: 'test-table',
        Item: {
            id: '1', // { S: '1' }ではなく'1'とする
            name: 'John',
            age: '20',
        },
    };
    const command = new PutItemCommand(params);
    try {
        await dynamodbClient.send(command);
        return {
            statusCode: 200,
            body: 'success',
        };
    } catch (err) {
        return {
            statusCode: 500,
            body: JSON.stringify({
                message: 'some error happened',
            }),
        };
    }
};

するとItem内のプロパティに以下のような型エラーが発生しました。

Type 'string' is not assignable to type 'AttributeValue'. ts(2322)

'1'というstring型のままだとDynamoDBClientのPutItemCommandInputにおいてDynamoDBレコードのデータ値の型 AttributeValueには適用できないのです。

つまり

@aws-sdk/lib-dynamodbDynamoDBDocumentClientを使うと、DynamoDBレコードのAttributeValueに従って厳密に記述せずにJavaScriptで取り扱うことのできるstringやnumberなどの型で記述することができます。

どっちを使う?

@aws-sdk/lib-dynamodbDynamoDBDocumentClientはJavaScriptの型を用いてシンプルに記述することができます。
コードの記述量が減るので開発を進めやすくなります。小規模だったり簡単な操作のみであれば DynamoDBDocumentClientを使うとよさそうです。
@aws-sdk/client-dynamodbDynamoDBClientはDynamoDBのAPIに直接アクセスすることで様々な操作が可能です。大量のデータを扱ったり、複雑な操作をしたりする場合(@aws-sdk/lib-dynamodbでは対応していない操作がある場合)は DynamoDBClient を使うことになります。
開発規模や状況に応じて、方針を決めて使い分けるとよいでしょう。

次に調べたいこと

今回はDynamoDBのデータ型やマーシャリング、アンマーシャリング等の深掘りはせずに簡単な処理の記述方法の違いについて書きました。
TypeScriptを用いてLambda関数コードを書くにあたり、データ型についてもさらに調べてみたいと思いました。

GitHubで編集を提案
Fusic 技術ブログ

Discussion