この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは!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のレコードとして、以下のようなレコードが存在しており、このレコードを更新することを目的としています。更新処理としては、この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仕様書をよく読んで利用すると良さそうでした。
どなたかのお役に立てば幸いです。それでは!