AWS SDK for JavaScript v3 x Next.js で DynamoDB に Query してみた

2022.09.09

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは!DA(データアナリティクス)事業本部 サービスソリューション部の大高です。

最近、Next.jsでAWS SDK for JavaScript v3を利用しているのですが、DynamoDB に対して Query する機会があったので、対応をまとめておきたいと思います。

前提条件

今回試した環境は以下のとおりです。

  • next
    • ^11.1.4
  • aws-amplify
    • ^4.3.22
  • @aws-sdk/client-dynamodb
    • ^3.121.0"

また、DynamoDB上のテーブルは、以下のようなテーブルを前提とします。

キー 名前 備考
パーティションキー id (String)
ソートキー type (String)
ローカルセカンダリインデックス(LSI) IdTypeIndex パーティションキー id (String), ソートキー type (String)

加えて、今回試す環境ではAmplifyのAuthenticatorを利用して認証していることを前提としています。詳細については以下のエントリもご参照ください。

API Routes で DynamoDB に Query する

まずは、API Routes を利用して DynamoDB に Query するコードです。

pages/api/query-api.ts

import type { Credentials } from '@aws-sdk/types'

import type { NextApiRequest, NextApiResponse } from 'next'

import { DynamoDBClient, QueryCommand, QueryCommandInput, QueryCommandOutput } from '@aws-sdk/client-dynamodb'
import { withSSRContext } from 'aws-amplify'

export default async function handler(req: NextApiRequest, res: NextApiResponse<QueryCommandOutput | Error>) {
  try {
    const body = JSON.parse(req.body)
    const SSR = withSSRContext({ req: req })
    const credentials: Credentials = await SSR.API.Auth.currentCredentials()
    const input: QueryCommandInput = body.input

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

    const command: QueryCommand = new QueryCommand(input)
    const response: QueryCommandOutput = await client.send(command)

    res.json(response)
  } catch (e) {
    console.log(e)
    res.status(500).json({ name: e.name, message: e.message })
  }
}

このAPIは、リクエストにQueryCommandInputの情報を渡すことで、DynamoDBにQueryコマンドを発行して結果を返すだけのAPIになっています。

このあたりはシンプルですね。

API の呼び出し元

次に、APIを呼び出す側のコードです。

libs/query.ts

import { AttributeValue, QueryCommandInput, QueryCommandOutput } from '@aws-sdk/client-dynamodb'

async function sleep(msec: number): Promise<unknown> {
  return new Promise((resolve) => {
    setTimeout(resolve, msec)
  })
}

async function callApi(input: QueryCommandInput): Promise<QueryCommandOutput> {
  const res = await fetch('/api/query-api', {
    method: 'POST',
    body: JSON.stringify({ input: input }),
  })
  const data = await res.json()
  if (!res.ok) {
    throw new Error(data.message)
  }

  return data as QueryCommandOutput
}

export async function queryItems(input: QueryCommandInput): Promise<Record<string, AttributeValue>[]> {
  const items: Record<string, AttributeValue>[] = []

  const query = async (input: QueryCommandInput) => {
    const response: QueryCommandOutput = await callApi(input)
    items.push(...response.Items)

    // 追加Scanデータがある場合には、追加Scanを実施する(Scan結果が1MBを超えるケース)
    // ref.) https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Scan.html
    if (response.LastEvaluatedKey) {
      // APIリクエストレートを制御する
      await sleep(500)

      input.ExclusiveStartKey = response.LastEvaluatedKey
      await query(input)
    }
  }

  await query(input)

  return items
}

callApiでは、単純にAPIに対してPOSTリクエストをしています。なお、このリクエストのbodyに対してQueryCommandInputをJSON化したものを渡してあげることで、API側でもQueryCommandInputを利用できるようにしています。

queryItemsでは、このcallApiの呼び出しを行い、取得された結果をitemsに溜めて返却しています。

ポイントとしては、LastEvaluatedKeyがある場合には、再帰的にQueryをしているところになります。

これはドキュメントに記載のあるとおり、Scan結果が1MBを超える場合にはレスポンスに全件含まれないためです。次回のリクエストのExclusiveStartKeyに対してLastEvaluatedKeyを設定することで、取得しきれなかったレコードを取得しています。

また、APIを再呼び出しする際には念の為少しsleepしてから呼び出すようにしてみました。

QueryCommandInput について

最後に、呼び出しに必要なQueryCommandInputについてです。

const input: QueryCommandInput = {
  TableName: 'SampleTable',
  IndexName: 'IdTypeIndex',
  KeyConditionExpression: 'id = :id AND type = :type',
  ExpressionAttributeValues: {
    ':id': { S: '51f66320-7233-48e9-a718-733735da4249' },
    ':type': { S: 'White Mage' },
  },
}

TableNameはDynamoDBのテーブル名ですね。IndexNameには今回利用したいローカルセカンダリインデックス(LSI)の名前IdTypeIndexを指定しています。

KeyConditionExpressionはキーでの検索条件です。ここでは実際のキー名idと置換名(コロン付きのもの):idをどちらも同じにしていますが、:myidのようにしてもOKです。

最後にExpressionAttributeValuesで、利用する実際の値を指定しています。先程の置換名(コロン付きのもの):id:typeに対して、検索したい値をDynamoDB形式で指定しています。

なお、DynamoDB形式での値の指定は少しクセがあるのですが、もうちょっと楽に記述したい場合には@aws-sdk/util-dynamodbmarshallを利用すると簡単になるかと思います。私は以下のエントリを参考にさせていただき、実装が楽になりました。

まとめ

以上、AWS SDK for JavaScript v3 x Next.js で DynamoDB に Query してみました。

DynamoDB はよく利用するので、今後はPutItemやDeleteItemについても書いていきたいと思います。

どなたかのお役に立てば幸いです。それでは!

参考