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

2022.09.30

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

最近、Next.jsでAWS SDK for JavaScript v3を利用しています。今回は、DynamoDB に対して UpdateItem したみたので、対応方法について書いていきたいと思います。

前提条件

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

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

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

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

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

API Routes で DynamoDB に UpdateItem する

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

pages/api/update-item-api.ts

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

import type { NextApiRequest, NextApiResponse } from 'next'

import {
  DynamoDBClient,
  UpdateItemCommand,
  UpdateItemCommandInput,
  UpdateItemCommandOutput,
} from '@aws-sdk/client-dynamodb'
import { withSSRContext } from 'aws-amplify'

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

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

    const command: UpdateItemCommand = new UpdateItemCommand(input)
    const response: UpdateItemCommandOutput = await client.send(command)

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

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

API の呼び出し元

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

libs/update-item.ts

import { UpdateItemCommandInput, UpdateItemCommandOutput } from '@aws-sdk/client-dynamodb'
import { marshall } from '@aws-sdk/util-dynamodb'
import { Item } from '@ebirah/utils/aws'

function toExpressionAttributeValues(attributes): Item {
  const exAttributes = {}

  for (const [key, value] of Object.entries(attributes)) {
    exAttributes[':' + key] = value
  }

  const expressionAttributeValues: Item = marshall(exAttributes)

  return expressionAttributeValues
}

function toUpdateExpression(attributes): string {
  let updateExpression = 'SET '

  for (const key of Object.keys(attributes)) {
    updateExpression += `${key} = :${key},`
  }
  updateExpression = updateExpression.substring(0, updateExpression.length - 1)

  return updateExpression
}

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

  return data as UpdateItemCommandOutput
}

export async function updateItem(): Promise<UpdateItemCommandOutput> {
  const updateItemKey = {
    id: '51f66320-7233-48e9-a718-733735da4249',
    type: 'White Mage',
  }

  const updateItemAttributes = {
    cap: 90,
  }

  const input: UpdateItemCommandInput = {
    TableName: 'SampleTable',
    Key: marshall(updateItemKey),
    ExpressionAttributeValues: toExpressionAttributeValues(updateItemAttributes),
    UpdateExpression: toUpdateExpression(updateItemAttributes),
    ReturnValues: 'ALL_NEW',
  }

  const result: UpdateItemCommandOutput = await callApi(input)

  return result
}

ややこしいので、少しずつ分けて解説していきます。

callApi

これは API Routes の API を呼び出すだけの function です。

callApi

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

  return data as UpdateItemCommandOutput
}

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

updateItem

これは、先程のcallApiの呼び出しを行い、取得された結果をそのまま返却する function です。

callApi

export async function updateItem(): Promise<UpdateItemCommandOutput> {
  const updateItemKey = {
    id: '51f66320-7233-48e9-a718-733735da4249',
    type: 'White Mage',
  }

  const updateItemAttributes = {
    cap: 90,
  }

  const input: UpdateItemCommandInput = {
    TableName: 'SampleTable',
    Key: marshall(updateItemKey),
    ExpressionAttributeValues: toExpressionAttributeValues(updateItemAttributes),
    UpdateExpression: toUpdateExpression(updateItemAttributes),
    ReturnValues: 'ALL_NEW',
  }

  const result: UpdateItemCommandOutput = await callApi(input)

  return result
}

ここではDynamoDBのレコードとして、以下のようなレコードが存在しており、このレコードを更新することを目的としています。更新処理としては、このcap80から90に更新したいと思います。

id (パーティションキー) type (ソートキー) cap
51f66320-7233-48e9-a718-733735da4249 White Mage 80

このため、updateItemKeyidtypeとその値を、updateItemAttributescapとその値を持った連想配列を定義しています。

UpdateItemCommandInputには、これらを用いて以下のように指定しています。

キー
TableName 対象のDynamoDBテーブル名
Key 更新時のキー
ExpressionAttributeValues UpdateExpressionで指定するAttribute名と値の情報
UpdateExpression 値を更新するアクションの式
ReturnValues UpdateItemCommandOutputで取得する値

ここも詳しく見ていきます。

TableName

これは単純にテーブル名を指定するだけなので、問題ないかと思います。

Key

これは更新する際のキーですが、DynamoDB形式で指定する必要があります。

つまり、以下のような形式になります。この記述を楽にするために@aws-sdk/util-dynamodbmarshallを利用しています。

{
  'id': {
    S: '51f66320-7233-48e9-a718-733735da4249'
  },
  'type': {
    S: 'White Mage'
  }
}

ExpressionAttributeValues

こちらは具体的には以下のようになります。

{
  ':cap': {
    N: '90'
  }
}

キーとしてコロン:を接頭辞とした値を指定します。一方で、この:capとしているcapの箇所は実際のDynamoDB上のカラム名と同じにする必要はありません。

単純にここで指定したものが、後述のUpdateExpressionで展開されるだけと考えると良さそうです。

今回は上記のようなオブジェクトを簡単に作り出したかったため、以下のようなfunctionを作って利用しています。

function toExpressionAttributeValues(attributes): Item {
  const exAttributes = {}

  for (const [key, value] of Object.entries(attributes)) {
    exAttributes[':' + key] = value
  }

  const expressionAttributeValues: Item = marshall(exAttributes)

  return expressionAttributeValues
}

UpdateExpression

こちらは具体的には以下のようになります。

SET cap = :cap

SETで指定した式の左側capが「実際のDynamoDB上のカラム名」で、右側:capが「ExpressionAttributeValues で指定したもの」になっています。

こちらも今回は上記のような式を簡単に作り出したかったため、以下のようなfunctionを作って利用しています。

function toUpdateExpression(attributes): string {
  let updateExpression = 'SET '

  for (const key of Object.keys(attributes)) {
    updateExpression += `${key} = :${key},`
  }
  updateExpression = updateExpression.substring(0, updateExpression.length - 1)

  return updateExpression
}

なお今回はSETだけを利用していますが、他にもREMOVEADDDELETEが利用できます。

ReturnValues

UpdateItemCommandOutputAttributesに何を返却して欲しいかを指定します。

それぞれ指定した値によって、以下が返却されます。

何が返却されるか
NONE 何も返却されない(デフォルト)
ALL_OLD 更新のレコードのすべての属性
UPDATED_OLD 更新のレコードの更新対象の属性
ALL_NEW 更新のレコードのすべての属性
UPDATED_NEW 更新のレコードの更新対象の属性

今回は「更新のレコードのすべての属性」が欲しかったので、ALL_NEWを指定しています。

処理結果としてどうなるか

ということで、処理結果としては以下のようになります。

更新前

id (パーティションキー) type (ソートキー) cap
51f66320-7233-48e9-a718-733735da4249 White Mage 80

更新後

id (パーティションキー) type (ソートキー) cap
51f66320-7233-48e9-a718-733735da4249 White Mage 90

まとめ

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

UpdateItemCommandはパラメータの指定がやや複雑なので、API仕様書をよく読んで利用すると良さそうでした。

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

参考

関連エントリ