[AWS AppSync] GraphQL で DynamoDB の UpdateItem をテンプレート (VTL) だけで組み立てる
AWS AppSync の GraphQL で DynamoDB のアイテムを更新する際、リゾルバーで UpdateItem を実行する必要があります。
GetItem の FilterExpression についてはユーティリティ関数の $util.transform.toDynamoDBFilterExpression
(リファレンス) でうまい具合に変換してくれるのですが、残念ながら UpdateItem 向けのユーティリティ関数は用意されていません。
今回はテンプレート言語である VTL (Apache Velocity Template Language) でがんばって JSON を生成してみます。
前提条件
想定する読者
- AppSync や GraphQL の初心者の方
- データソースに DynamoDB を使用している方
- DynamoDB の扱い方はある程度わかっている方
公式情報
そもそも AppSync には頼れる情報源がほぼ公式情報しかないありませんので、このあたりを参考にします。
- DynamoDB のリゾルバー
DynamoDB のリゾルバーのマッピングテンプレートリファレンス - AWS AppSync - マッピングテンプレートのプログラミング方法
リゾルバーのマッピングテンプレートプログラミングガイド - AWS AppSync - DynamoDB 用のユーティリティ関数
$util.dynamodb の DynamoDB ヘルパー - リゾルバーのマッピングテンプレートのユーティリティリファレンス - AWS AppSync
肝心の VTL については Apache の公式リファレンス (英語) しかないですが、がんばりましょう。
DynamoDB テーブル
こんな感じの DynamoDB のテーブルを想定します。 key
属性がパーティションキーです。
key | name | |
---|---|---|
264c399b-aa38-46ae-919b-b99d049a26af | ほげ ほげ男 | hoge-hogeo@example.com |
d6fca8ed-1339-492f-88f0-cba9a262c88d | 大阪 花子 | osaka-hanako@example.com |
GraphQL スキーマ定義
GraphQL のスキーマ定義は下記を想定します。
type Item {
key: ID!
name: String
email: String
}
input ItemInput {
name: String
email: String
}
type Mutation {
updateItem(key: ID!, input: ItemInput): Item
}
更新したい内容
ここでテーブル内の ほげ ほげ男
さんの名前とメールアドレスを更新したいとします。
GraphQL としてはミューテーション updateItem
を呼び出す際に
key
に264c399b-aa38-46ae-919b-b99d049a26af
input
に下記のようなハッシュマップ
{
"name": "ほげ 太郎",
"email": "hogetaro@example.com"
}
を渡せばいいわけです。
リゾルバー
テンプレートの出力結果から考える
さて、いよいよリゾルバーのお話です。
AppSync のマッピングテンプレートに使用する VTL はその名のとおりテンプレート言語ですので、最終的には UpdateItem の JSON が出力されるように記述しなければなりません。
要するに最終的に下記のような JSON を出力する必要があります。
{
"version" : "2018-05-29",
"operation" : "UpdateItem",
"key": {
"key" : { "S": "264c399b-aa38-46ae-919b-b99d049a26af" }
},
"update" : {
"expression" : "SET #name = :name, #email = :email",
"expressionNames" : {
"#name" : "name",
"#email" : "email"
},
"expressionValues" : {
":name" : { "S" : "ほげ 太郎" },
":email" : { "S" : "hogetaro@example.com" }
}
}
}
はい、すでにややこしいですね。しかしこれは AppSync というより DynamoDB の仕様上 UpdateItem の表現がややこしいので仕方ありません。
UpdateItem の詳細についてはここでは割愛します。下記のリファレンスを参照してください。
expressionNames
は必ずしも定義する必要はありませんが、テーブルの属性名が DynamoDB の定義語のときにエラーになりますので、いっそすべて定義したほうがわかりやすいでしょう。
これぐらいの属性数であれば上記のようにベタ書きでもいけますが、属性が多い場合は修正も大変ですしバグのもとになりそうです。そこで VTL のループとユーティリティ関数を利用して、動的に生成します。
マッピングテンプレート
今回のリクエストマッピングテンプレートの全体像です。
#set($expression = "")
#set($expressionNames = {})
#set($expressionValues = {})
#foreach($key in $ctx.args.input.keySet())
#if ($expression.length().equals(0))
#set($expression = "SET #$key = :$key")
#else
#set($expression = "${expression}, #$key = :$key")
#end
$util.qr($expressionNames.put("#$key", $key))
$util.qr($expressionValues.put(":$key", $ctx.args.input.get($key)))
#end
{
"version" : "2018-05-29",
"operation" : "UpdateItem",
"key" : {
"key": $util.dynamodb.toDynamoDBJson($ctx.args.key)
},
"update" : {
"expression": "$expression",
"expressionNames": $util.toJson($expressionNames),
"expressionValues": $util.dynamodb.toMapValuesJson($expressionValues)
}
}
後半に最終的に出力すべき UpdateItem の JSON が表れていますね。では順番に見ていきます。
変数の準備
#set($expression = "")
#set($expressionNames = {})
#set($expressionValues = {})
それぞれ JSON の update
の中に展開する expression
, expressionNames
, expressionValues
を生成していくため、変数を初期化しています。
ループ処理
#foreach($key in $ctx.args.input.keySet())
ItemInput で渡ってきたハッシュマップの内容をループ処理するため、キーの一覧を取得して foreach で回します。
JavaScript でいえば下記のようなイメージです。
for (var $key of Object.keys($ctx.args.input))
このループ内で expression
, expressionNames
, expressionValues
を生成します。
Expression の構築
まず UpdateExpression を $expression
に生成します。最終的に SET #name = :name, #email = :email
としたいわけです。
#if ($expression.length().equals(0))
#set($expression = "SET #$key = :$key")
#else
#set($expression = "${expression}, #$key = :$key")
#end
初回のみ "SET #$key = :$key"
を代入し、2回目以降は , #$key = :$key
を追加していくという流れです。
本当は JavaScript でいう map や join が使えればもう少しスマートに書けますが、 VTL では提供されていないようなので、この方法にしました。
ExpressionAttributeNames の構築
次に ExpressionAttributeNames を $expressionNames
に構築していきます。こちらは最終的に下記のようになればよいので、キーに属性名に #
をつけたもの、バリューにはそのまま属性名を設定します。
{
"#name" : "name",
"#email" : "email"
}
空のマップに put
でキー・バリューを追加していくだけです。
$util.qr($expressionNames.put("#$key", $key))
ExpressionAttributeValues の構築
次に ExpressionAttributeValues を $expressionValues
に構築していきます。こちらは最終的に下記のようになればよいのですが、
"expressionValues" : {
":name" : { "S" : "ほげ 太郎" },
":email" : { "S" : "hogetaro@example.com" }
}
DynamoDB の型付きの JSON 表現にはユーティリティ関数の $util.dynamodb.toMapValuesJson
でまとめて変換できるため、キーにパラメーター表現(属性名に :
をつけたもの)、バリューに生の値とする下記のようなマップにします。
"expressionValues" : {
":name" : "ほげ 太郎",
":email" : "hogetaro@example.com"
}
Names と同様にマップに追加していくだけです。値自体は ItemInput のマップから get
で取得します。
$util.qr($expressionValues.put(":$key", $ctx.args.input.get($key)))
JSON の出力
最後に最終型の JSON を出力します。
{
"version" : "2018-05-29",
"operation" : "UpdateItem",
"key" : {
"key": $util.dynamodb.toDynamoDBJson($ctx.args.key)
},
"update" : {
"expression": "$expression",
"expressionNames": $util.toJson($expressionNames),
"expressionValues": $util.dynamodb.toMapValuesJson($expressionValues)
}
}
key
部分には $util.dynamodb.toDynamoDBJson
で DynamoDB の型付き表現に変換します。
expression
はすでに "SET #name = :name, #email = :email"
という形式が入っていますが、そのまま展開すると下記のようになってしまいます。
{
"key": SET #name = :name, #email = :email
}
そのためダブルクオーテーションで括り、 "$expression"
としています。これで下記のように展開されるはずです。
{
"key": "SET #name = :name, #email = :email"
}
expressionNames
はそのまま JSON にすればよいので $util.toJson
で変換します。
expressionValues
は先述のとおり、 DynamoDB の型付き表現を含む JSON にする必要があるため、 $util.dynamodb.toMapValuesJson
を使って変換します。
このあたりのユーティリティ関数は下記の公式リファレンスを参照してください。
これで UpdateItem ができるはずです。エラーになった場合に原因を特定するのが非常に手間取りますが、…がんばりましょう。
まとめ
AWS AppSync の GraphQL で DynamoDB のアイテムを更新するためにリゾルバーで UpdateItem の JSON を VTL で生成する方法を紹介しました。
複雑な変換処理であれば Lambda で書いたほうが早いかもしれませんが、せっかくなので VTL だけで書いて、シンプルな構成を維持したいところです。
もっとよい方法をご存知の方がいらっしゃればご教示いただけますと幸いです。