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







やりたいこと
以下のような処理をしたいです。
- アプリからの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 を使っています。
本記事が皆様のお役に立てれば幸いです。

