AWS SDK for JavaScript v3 x Next.js で DynamoDB に UpdateItem してみた
こんにちは!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 するコードです。
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を呼び出す側のコードです。
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 です。
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 です。
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のレコードとして、以下のようなレコードが存在しており、このレコードを更新することを目的としています。更新処理としては、このcap
を80
から90
に更新したいと思います。
id (パーティションキー) | type (ソートキー) | cap |
---|---|---|
51f66320-7233-48e9-a718-733735da4249 | White Mage | 80 |
このため、updateItemKey
にid
、type
とその値を、updateItemAttributes
にcap
とその値を持った連想配列を定義しています。
UpdateItemCommandInput
には、これらを用いて以下のように指定しています。
キー | 値 |
---|---|
TableName | 対象のDynamoDBテーブル名 |
Key | 更新時のキー |
ExpressionAttributeValues | UpdateExpressionで指定するAttribute名と値の情報 |
UpdateExpression | 値を更新するアクションの式 |
ReturnValues | UpdateItemCommandOutputで取得する値 |
ここも詳しく見ていきます。
TableName
これは単純にテーブル名を指定するだけなので、問題ないかと思います。
Key
これは更新する際のキーですが、DynamoDB形式で指定する必要があります。
つまり、以下のような形式になります。この記述を楽にするために@aws-sdk/util-dynamodb
のmarshall
を利用しています。
{ '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
だけを利用していますが、他にもREMOVE
、ADD
、DELETE
が利用できます。
ReturnValues
UpdateItemCommandOutput
のAttributes
に何を返却して欲しいかを指定します。
それぞれ指定した値によって、以下が返却されます。
値 | 何が返却されるか |
---|---|
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仕様書をよく読んで利用すると良さそうでした。
どなたかのお役に立てば幸いです。それでは!