AWS AppSync リゾルバ (VTL) の書き方サンプル No.7 – Amazon DynamoDB 応用編 2つのクエリを直列に実行

こんにちは、広野です。

AWS AppSync を使用したアプリケーションを開発する機会があり、リゾルバ、主に VTL の書き方に関してまとまった知識が得られたので紹介します。前回からの続きもので、今回は 1つの VTL で 2つのクエリを直列に実行する方法 (パイプラインリゾルバ) を紹介します。

本記事では、VTL の書き方にフォーカスしています。ご了承ください。
AWS AppSync、リゾルバ、VTL の説明については以下の記事をご覧下さい。
AWS AppSync リゾルバ (VTL) の書き方サンプル No.1 - Amazon DynamoDB GetItem
Amazon DynamoDB に VTL で GetItem をかけるときの基本的な書き方を紹介します。
AWS AppSync リゾルバ (VTL) の書き方サンプル No.2 – Amazon DynamoDB BatchGetItem
Amazon DynamoDB に VTL で BatchGetItem をかけるときの基本的な書き方を紹介します。
AWS AppSync リゾルバ (VTL) の書き方サンプル No.3 – Amazon DynamoDB Query
Amazon DynamoDB に VTL で Query をかけるときの基本的な書き方を紹介します。
AWS AppSync リゾルバ (VTL) の書き方サンプル No.4 - Amazon DynamoDB PutItem
Amazon DynamoDB に VTL で PutItem をかけるときの基本的な書き方を紹介します。
AWS AppSync リゾルバ (VTL) の書き方サンプル No.5 – Amazon DynamoDB UpdateItem
Amazon DynamoDB に VTL で UpdateItem をかけるときの基本的な書き方を紹介します。
AWS AppSync リゾルバ (VTL) の書き方サンプル No.6 - Amazon DynamoDB 応用編 引数内のフラグにより処理を分ける
Amazon DynamoDB に VTL でデータ読み書きをさせるときに、引数内のフラグにより実行させる処理を分岐する方法を紹介します。
AWS AppSync を使って React アプリからキックした非同期ジョブの結果をプッシュ通知で受け取る
非同期ジョブを実行した後、結果をどう受け取るか?というのは開発者として作り込み甲斐のあるテーマです。今回は React アプリが非同期ジョブを実行した後に、AWS AppSync 経由でジョブ完了のプッシュ通知を受け取る仕組みを紹介します。

やりたいこと

以下のような処理をしたいです。

  • アプリからの1回のリクエストを受けて、リゾルバの中で直列した 2 つの処理を実行した結果をアプリにレスポンスする。
  • アプリへのレスポンスは、1つ目の処理 (関数1) の結果を 2つ目の処理 (関数2) が受け取り、処理に使用する。

これは、以下のようなパイプラインリゾルバを構築することで実現できます。

公式には、以下のドキュメントをご覧下さい。

本記事のサンプルでは、アプリから AWS AppSync に「グラフに使用する 3 つのデータを取得する」リクエストを受けたとします。Amazon DynamoDB には適切なデータがある想定です。テーブル名はリゾルバの別の設定 (Data Source) で行います。

  • 1つ目の関数: パーティションキー pkey、ソートキー skey1 が “TRUE” である件数をテーブル A から取得
  • 2つ目の関数: パーティションキー pkey、ソートキー skey1 が “FALSE” である件数をテーブル A から取得
  • 3つ目の関数: パーティションキー pkey、ソートキー skey2 が “Red” から始まる件数をテーブル B から取得
  • pkey は pkey という変数名で引数としてアプリから値を渡される

これらの件数を、以下のフォーマットの JSON データとしてまとめてレスポンスする。

{
  "true": xx,
  "false": xx,
  "red": xx
}

Amazon DynamoDB に命令する VTL

まず、大元締めのパイプラインリゾルバのマッピングテンプレートをつくりますが、実体はほぼ空です。実処理は各関数のリゾルバに記述します。

パイプラインリゾルバのリクエストマッピングテンプレート

何もしません。(笑)

{}

パイプラインリゾルバのレスポンスマッピングテンプレート

最後に実行された関数から受け取ったレスポンスをそのままパイプラインリゾルバのレスポンスとしてアプリに返す仕様にします。

$util.toJson($context.result)

関数 1 のリクエストマッピングテンプレート

{
  "version": "2018-05-29",
  "operation": "Query",
  "query": {
    "expression": "#pkey = :pkey AND #skey1 = :skey1",
    "expressionNames": {
      "#pkey": "pkey",
      "#skey1": "skey1"
    },
    "expressionValues": {
      ":pkey": $util.dynamodb.toDynamoDBJson($context.arguments.pkey),
      ":skey1": $util.dynamodb.toDynamoDBJson("TRUE")
    }
  },
  "nextToken": $util.toJson($util.defaultIfNullOrEmpty($context.arguments.after, null)),
  "scanIndexForward": false,
  "consistentRead": false,
  "projection": {
    "expression": "pkey"
  }
}

シンプルな、条件に合致する行の件数をカウントしたいだけのクエリなので、projection に pkey のみを指定しています。これによりレスポンスのデータ量を節約します。

関数 1 のレスポンスマッピングテンプレート

結果のうち、件数だけを取得したいので、以下のように scannedCount を使用します。

$util.toJson({
  "true": $ctx.result.scannedCount
})

このレスポンスは、関数 2 に渡されます。

関数 2 のリクエストマッピングテンプレート

{
  "version": "2018-05-29",
  "operation": "Query",
  "query": {
    "expression": "#pkey = :pkey AND #skey1 = :skey1",
    "expressionNames": {
      "#pkey": "pkey",
      "#skey1": "skey1"
    },
    "expressionValues": {
      ":pkey": $util.dynamodb.toDynamoDBJson($context.arguments.pkey),
      ":skey1": $util.dynamodb.toDynamoDBJson("FALSE")
    }
  },
  "nextToken": $util.toJson($util.defaultIfNullOrEmpty($context.arguments.after, null)),
  "scanIndexForward": false,
  "consistentRead": false,
  "projection": {
    "expression": "pkey"
  }
}

本サンプルでは、関数 2 は関数 1 のソートキーの条件を変えただけのもので、ほとんど同じです。skey1 が FALSE である件数を取得する目的です。

関数 2 のレスポンスマッピングテンプレート

ここは関数 1 とは少し変わります。関数 1 のレスポンスを受け取ったものをここでマージします。

$util.qr($ctx.prev.result.put("false", $ctx.result.scannedCount))
$util.toJson($ctx.prev.result)

$ctx.prev.result が関数 1 のレスポンスです。そこに “false”: に続けて関数 2 の結果 $ctx.result の中の件数に当たる scannedCount を取り出してマージしています。結果として、以下のデータが関数 2 のレスポンスとして関数 3 に渡されます。

{
  "true": xx,
  "false": xx
}

関数 3 のリクエストマッピングテンプレート

こちらは、関数 1,2 とは別のテーブルから別のデータの件数を取得します。

{
  "version": "2018-05-29",
  "operation": "Query",
  "query": {
    "expression": "#pkey = :pkey AND begins_with(#skey2, :skey2)",
    "expressionNames": {
      "#pkey": "pkey",
      "#skey2": "skey2"
    },
    "expressionValues": {
      ":pkey": $util.dynamodb.toDynamoDBJson($context.arguments.pkey),
      ":skey2": $util.dynamodb.toDynamoDBJson("Red")
    }
  },
  "nextToken": $util.toJson($util.defaultIfNullOrEmpty($context.arguments.after, null)),
  "scanIndexForward": false,
  "consistentRead": false,
  "projection": {
    "expression": "pkey"
  }
}

関数 3 のレスポンスマッピングテンプレート

関数 2 から受け取ったレスポンスをここでマージします。やっていることは関数 2 と同じです。

$util.qr($ctx.prev.result.put("red", $ctx.result.scannedCount))
$util.toJson($ctx.prev.result)

結果として、以下のデータが関数 3 のレスポンスとしてパイプラインリゾルバに返されます。パイプラインリゾルバは今回の例ではそのままアプリにデータを返すので、実質関数 3 のレスポンスが最終的なレスポンスになります。

{
  "true": xx,
  "false": xx,
  "red": xx
}

(参考) AWS CloudFormation テンプレート

私はこの面倒なリゾルバの定義を AWS マネジメントコンソールで設定する気が起きず (UI がわかりにくいので)、AWS CloudFormation でデプロイしています。完全な AWS AppSync のテンプレートではありませんが、パイプラインリゾルバのところの Resources のみ抜粋して紹介します。

  # パイプラインリゾルバの定義
  AppSyncResolverPipeline:
    Type: AWS::AppSync::Resolver
    Properties:
      ApiId: !GetAtt AppSyncApi.ApiId # AppSync の API ID
      TypeName: Query
      FieldName: queryUserExamScore # AppSync のスキーマで定義したクエリの名前
      Kind: PIPELINE # ここでパイプラインリゾルバであることを宣言
      PipelineConfig:
        Functions: # 作成したい関数を処理順に並べる、関数 ID を参照
          - !GetAtt FunctionUserExamScore1true.FunctionId
          - !GetAtt FunctionUserExamScore2false.FunctionId
          - !GetAtt FunctionUserExamScore3red.FunctionId
      RequestMappingTemplate: |
        {}
      ResponseMappingTemplate: |
        $util.toJson($context.result)
    DependsOn:
      - FunctionUserExamScore1true
      - FunctionUserExamScore2false
      - FunctionUserExamScore3red

  # 関数 1 の定義
  FunctionUserExamScore1true:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId: !GetAtt AppSyncApi.ApiId
      # DynamoDB テーブル A のデータソース名をここで参照
      DataSourceName: !GetAtt AppSyncDataSourceTableA.Name
      Description: AppSync Function for User Exam Score Graph 1 for true
      FunctionVersion: 2018-05-29
      Name: AppSyncFunctionUserExamScore1true
      RequestMappingTemplate: |
        {
          "version": "2018-05-29",
          "operation": "Query",
          "query": {
            "expression": "#pkey = :pkey AND #skey1 = :skey1",
            "expressionNames": {
              "#pkey": "pkey",
              "#skey1": "skey1"
            },
            "expressionValues": {
              ":pkey": $util.dynamodb.toDynamoDBJson($context.arguments.pkey),
              ":skey1": $util.dynamodb.toDynamoDBJson("TRUE")
            }
          },
          "nextToken": $util.toJson($util.defaultIfNullOrEmpty($context.arguments.after, null)),
          "scanIndexForward": false,
          "consistentRead": false,
          "projection": {
            "expression": "pkey"
          }
        }
      ResponseMappingTemplate: |
        $util.toJson({
          "true": $ctx.result.scannedCount
        })
    DependsOn:
      - AppSyncDataSourceTableA

  # 関数 2 の定義
  FunctionUserExamScore2false:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId: !GetAtt AppSyncApi.ApiId
      # DynamoDB テーブル A のデータソース名をここで参照
      DataSourceName: !GetAtt AppSyncDataSourceTableA.Name
      Description: AppSync Function for User Exam Score Graph 2 for false
      FunctionVersion: 2018-05-29
      Name: AppSyncFunctionUserExamScore2false
      RequestMappingTemplate: |
        {
          "version": "2018-05-29",
          "operation": "Query",
          "query": {
            "expression": "#pkey = :pkey AND #skey1 = :skey1",
            "expressionNames": {
              "#pkey": "pkey",
              "#skey1": "skey1"
            },
            "expressionValues": {
              ":pkey": $util.dynamodb.toDynamoDBJson($context.arguments.pkey),
              ":skey1": $util.dynamodb.toDynamoDBJson("FALSE")
            }
          },
          "nextToken": $util.toJson($util.defaultIfNullOrEmpty($context.arguments.after, null)),
          "scanIndexForward": false,
          "consistentRead": false,
          "projection": {
            "expression": "pkey"
          }
        }
      ResponseMappingTemplate: |
        $util.qr($ctx.prev.result.put("false", $ctx.result.scannedCount))
        $util.toJson($ctx.prev.result)
    DependsOn:
      - AppSyncDataSourceTableA

  # 関数 3 の定義
  FunctionUserExamScore3red:
    Type: AWS::AppSync::FunctionConfiguration
    Properties:
      ApiId: !GetAtt AppSyncApi.ApiId
      # DynamoDB テーブル B のデータソース名をここで参照
      DataSourceName: !GetAtt AppSyncDataSourceTableB.Name
      Description: AppSync Function for User Exam Score Graph 3 for red
      FunctionVersion: 2018-05-29
      Name: AppSyncFunctionUserExamScore3red
      RequestMappingTemplate: |
        {
          "version": "2018-05-29",
          "operation": "Query",
          "query": {
            "expression": "#pkey = :pkey AND begins_with(#skey2, :skey2)",
            "expressionNames": {
              "#pkey": "pkey",
              "#skey2": "skey2"
            },
            "expressionValues": {
              ":pkey": $util.dynamodb.toDynamoDBJson($context.arguments.pkey),
              ":skey2": $util.dynamodb.toDynamoDBJson("Red")
            }
          },
          "nextToken": $util.toJson($util.defaultIfNullOrEmpty($context.arguments.after, null)),
          "scanIndexForward": false,
          "consistentRead": false,
          "projection": {
            "expression": "pkey"
          }
        }
      ResponseMappingTemplate: |
        $util.qr($ctx.prev.result.put("red", $ctx.result.scannedCount))
        $util.toJson($ctx.prev.result)
    DependsOn:
      - AppSyncDataSourceTableB

まとめ

いかがでしたでしょうか。

今回のサンプルでは、それぞれの関数の結果を順番にマージしていくだけの簡単な直列処理でしたが、前の関数の結果を次の関数のクエリのパラメータに使用することもできます。その場合はリクエストマッピングテンプレートの中で前の関数のレスポンス $ctx.prev.result を使用します。

それにより通常 DynamoDB への単体のクエリでは実現できない、テーブルの結合に近いことが実はできます。まあ同じことやるにしても Lambda 関数書けば普通にできるんですが、VTL の方が処理が速いのと Lambda 関数が乱立しなくていいなぁと思って極力私は VTL を使っています。

本記事が皆様のお役に立てれば幸いです。

著者について
広野 祐司

AWS サーバーレスアーキテクチャを駆使して社内クラウド人材育成アプリとコンテンツづくりに勤しんでいます。React で SPA を書き始めたら快適すぎて、他の言語には戻れなくなりました。サーバーレス & React 仲間を増やしたいです。AWSは好きですが、それよりもフロントエンド開発の方が好きでして、バックエンド構築を簡単にしてくれたAWSには感謝の気持ちの方が強いです。
取得資格:AWS 認定は13資格、ITサービスマネージャ、ITIL v3 Expert 等
2020 - 2024 Japan AWS Top Engineer 受賞
2022 - 2024 AWS Ambassador 受賞
2023 当社初代フルスタックエンジニア認定
好きなAWSサービス:AWS Amplify / AWS AppSync / Amazon Cognito / AWS Step Functions / AWS CloudFormation

広野 祐司をフォローする
クラウドに強いによるエンジニアブログです。
SCSKは専門性と豊富な実績を活かしたクラウドサービス USiZE(ユーサイズ)を提供しています。
USiZEサービスサイトでは、お客様のDX推進をワンストップで支援するサービスの詳細や導入事例を紹介しています。
AWSアプリケーション開発クラウドソリューション
シェアする
タイトルとURLをコピーしました