AWS AppSyncのDynamoDBリゾルバーで文字列セット、数値セット、バイナリセットを扱う

2022.01.29

ども、ゲストブロガーのNTT東日本 大瀧です。 GraphQL APIを提供するマネージドサービスAppSyncは、No SQLデータベースであるDynamoDBをバックエンド(AppSyncではリゾルバーと呼ぶ)に指定できます。さらにAPIとデータベースを紐づけるマッピングテンプレートは、インポートとウィザードによる自動生成で簡単に設定できて便利です。ただし自動生成で対応できる範囲を超える場合は手直しが必要なケースがあります。本ブログではその例として、DynamoDBのセット型をAppSyncで扱う方法をご紹介します。

自動生成されるマッピングテンプレートの様子

たとえば今回は、DynamoDBテーブルのアトリビュートに文字列セットを持つ場合を取り上げます。tags という文字列セットを持つアイテムをAppSyncから参照・操作する例としましょう。

AppSyncのDynamoDBインポートウィザードでは、モデルとしてDynamoDBテーブルのアトリビュートをテーブル定義に合わせてセットしますが、GraphQLのスキーマでは文字列セットと文字列のリストを区別が無いことから文字列セットをタイプ一覧から選択できないため、ここではいったん文字列のリストと定義します。

クエリエクスプローラでクエリを発行する場合、DynamoDBの文字列セットは生成されたレスポンスマッピングテンプレートによって文字列のリストとして解釈されるため、たまたまスキーマと矛盾なく正常に処理できます。

今度はミューテーションを発行してみます。

mutation MyMutation {
  createPosts(input: {
    description: "ども", 
    tags: ["etc"], 
    title: "テストブログ2"
  }) {
    id
    tags
    title
    description
  }
}

こちらは生成されたリクエストマッピングテンプレートによって文字列のリストとしてそのままDynamoDBに書き込まれるため、文字列セットとして扱うことができません。以下の例では、2レコード目の tags が文字列セットではなく文字列のリストになっていることがわかります。

マッピングテンプレートの調整

ミューテーションに紐づくリクエストマッピングテンプレートが文字列セットを正しく扱えるよう、修正していきます。AWS管理コンソールのAppSyncのスキーマ定義画面の右側のカラムからMutationのリゾルバーの項目にスクロールし、ミューテーションごとのデータソース名のリンク(今回はDynamoDBテーブル名のposts)をクリックしそれぞれのマッピングテンプレート画面を開いていきます。今回は修正が比較的シンプルなUpdateミューテーションのリンクからクリックします。

それなりの分量が生成されていますが、今回関係するのはそのごく一部です。

テンプレートの記法は、以下のドキュメントが参考になります。

Updateミューテーション

今回関係する部分を抜粋してみます。

  #set( $expValues = {} )
    : (中略)
  ## Iterate through each argument, skipping keys **
  #foreach( $entry in $util.map.copyAndRemoveAllKeys($ctx.args.input, ["id"]).entrySet() )
    #if( $util.isNull($entry.value) )
      : (中略)
    #else
      ## Otherwise set (or update) the attribute on the item in DynamoDB **

      $!{expSet.put("#${entry.key}", ":${entry.key}")}
      $!{expNames.put("#${entry.key}", "${entry.key}")}
      $!{expValues.put(":${entry.key}", $util.dynamodb.toDynamoDB($entry.value))}
    #end
  #end
    : (中略)
  ## Finally, write the update expression into the document, along with any expressionNames and expressionValues **
  "update": {
    "expression": "${expression}",
    #if( !${expNames.isEmpty()} )
      "expressionNames": $utils.toJson($expNames),
    #end
    #if( !${expValues.isEmpty()} )
      "expressionValues": $utils.toJson($expValues),
    #end
  },

抜粋した先頭行では、投入する値を格納するexpValues配列を定義(#set)しています。続いての#foreachループは、インデックス(今回はid)を除いて入力データをDynamoDBの形式に整形しています。そのうちハイライトした1行が今回の目玉、DynamoDBのデータ型に変換する処理として $util.dynamodb.toDynamoDB() ヘルパーを呼んでいます。これは↓のドキュメントに記載があり、文字列セットに対応していません。

入力オブジェクトを該当する DynamoDB 表現に変換する、DynamoDB 用の一般的なオブジェクト変換ツール。このツールは、一部の型の表現方法に関して融通が利きません。たとえば、セット型 ("SS"、"NS"、"BS") ではなくリスト型 ("L") が使用されます。このプログラムは、DynamoDB の属性値を記述したオブジェクトを返します。

このヘルパーを各セットのデータ型に合わせたヘルパー呼び出しに代えることで対処できます。

  • $util.dynamodb.toStringSet : 文字列セット型に変換
  • $util.dynamodb.toNumberSet : 数値セット型に変換
  • $util.dynamodb.toBinarySet : バイナリセット型に変換

ここでは、条件分岐で呼び分けるようにしてみます。今回はtagsアトリビュートが文字列セットであることがわかっていますので、アトリビュート名となる$entry.key#ifの条件にしてみました。

    #else
      ## Otherwise set (or update) the attribute on the item in DynamoDB **

      $!{expSet.put("#${entry.key}", ":${entry.key}")}
      $!{expNames.put("#${entry.key}", "${entry.key}")}
      #if($entry.key == "tags")
        $!{expValues.put(":${entry.key}", $util.dynamodb.toStringSet($entry.value))}
      #else
        $!{expValues.put(":${entry.key}", $util.dynamodb.toDynamoDB($entry.value))}
      #end
    #end

これでUpdateミューテーションはOKです。続いてCreateミューテーションを見てみましょう。

Createミューテーション

{
  "version": "2017-02-28",
  "operation": "PutItem",
  "key": {
    "id": $util.dynamodb.toDynamoDBJson($util.autoId()),
  },
  "attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input),
  "condition": {
    "expression": "attribute_not_exists(#id)",
    "expressionNames": {
      "#id": "id",
    },
  },
}

こちらは入力がアトリビュートのみ(入力にインデックスが無く、$util.autoId()で自動生成する)という前提で全要素まとめての変換になっているため、ここを配列の定義とループ処理に置き換えてUpdateミューテーションと同様の処理にしてみましょう。

{
  "version": "2017-02-28",
  "operation": "PutItem",
  "key": {
    "id": $util.dynamodb.toDynamoDBJson($util.autoId()),
  },
  #set( $attValues = {} )
  #foreach( $entry in $ctx.args.input.entrySet() )
    #if( !$util.isNull($entry.value) )
      #if($entry.key == "tags")
        $!{attValues.put("${entry.key}", $util.dynamodb.toStringSet($entry.value))}
      #else
        $!{attValues.put("${entry.key}", $util.dynamodb.toDynamoDB($entry.value))}
      #end
    #end
  #end
  "attributeValues": $utils.toJson($attValues),
  "condition": {
    "expression": "attribute_not_exists(#id)",
    "expressionNames": {
      "#id": "id",
    },
  },
}

配列を$attValuesと定義し、入力でループさせて配列に追加、できたものをattributeValues要素の値にセットしてみました。

動作確認

では、クエリエクスプローラからCreateミューテーションを実行してみます。

DynamoDBテーブルを確認してみると。。。

きちんと文字列セットで書き込まれていますね!

まとめ

AWS AppSyncのDynamoDBリゾルバーで文字列セット、数値セット、バイナリセットを扱うために、マッピングテンプレートを変更する様子をご紹介しました。マッピングテンプレートはVTLの知識が求められるので少し敷居は高いですが、その分カスタマイズすることで今回のようなDynamoDB特有の要件にも対応できる柔軟性があります。皆さんもマッピングテンプレートのいろいろな活用法を見つけて便利に使っていきましょう。