Amplifyでトランザクション処理を行う

# はじめに
アドベントカレンダーに初参加させていただきます。
トライ＆エラーで進めている部分あるので、誤りや不足がある場合はご指摘よろしくお願いします。

# Amplifyとの出会い
元々はAWS Elastic Beanstalk（+RDS+CakePHP）を使って開発していました。

今年９月からの個人的な開発でReactを使い始めた際に、バックエンドどうしよう、認証を自前で作りたくないなと思っていたところ、Coginitoも使えるAmplify良さそうと思い、使い始めました。

AppSyncもDynamoもAmplifyで初めて使うので、問題発生時に「どこの問題なのかわからない状態」に陥ったりしますが、バックエンド含めてソースコードで管理できるのは便利だなと思い、楽しく使わせてもらっています。

# 今回やりたかったこと
ReactからAppSyncを呼び出して、Dynamoの複数テーブル、複数レコードを１つのトランザクションで更新したい。

# 結論
カスタムリゾルバで作成しました。
[マニュアル](https://docs.amplify.aws/cli/graphql-transformer/resolvers#add-a-custom-resolver-that-targets-a-dynamodb-table-from-model)

ドキュメントはQueryのカスタムリゾルバですが、Mutationもほとんど同じでできました。
以下マニュアルの手順同様にMutationの場合にどうしたかを記載します。

# 対応内容
## schema.graphqlにMutationを追加

今回はシンプルに`Aaa`テーブルと`Bbb`テーブルを用意して、それぞれに対する複数行の更新処理を1つのトランザクションで更新するイメージです。

`createAaaAndBbb`Mutationを追加して、それに必要な`AaaInput`,`BbbInput`,`AaasAndBbbs`を追加します。
ちなみにこの方法だと`Aaa`と`AaaInput`の項目を常に合わせる必要があり、不便ですがもっといい方法があるのかも。。

```schema.graphql
type Aaa @model{
  id: ID
  name: String!
}

type Bbb @model {
  id: ID
  name: String!
}

input AaaInput {
  id: ID
  name: String!
}

input BbbInput {
  id: ID
  name: String!
}

type AaasAndBbbs{
  aaas: [Aaa]
  bbbs: [Bbb]
}

type Mutation {
  createAaasAndBbbs(
    aaas: [AaaInput]
    bbbs: [BbbInput]
  ): AaasAndBbbs
}
```

# リゾルバーリソースをスタックに追加

`./amplify/backend/api/[project]/stacks/AaaAndBbbResources.json`を作成します。
デフォルトで配置されていた`CustomResources.json`をコピーして「//変更」と記載した部分のみ修正しました。

```
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "An auto-generated nested stack.",
  "Metadata": {},
  "Parameters": {
    "AppSyncApiId": {
      "Type": "String",
      "Description": "The id of the AppSync API associated with this project."
    },
    "AppSyncApiName": {
      "Type": "String",
      "Description": "The name of the AppSync API",
      "Default": "AppSyncSimpleTransform"
    },
    "env": {
      "Type": "String",
      "Description": "The environment name. e.g. Dev, Test, or Production",
      "Default": "NONE"
    },
    "S3DeploymentBucket": {
      "Type": "String",
      "Description": "The S3 bucket containing all deployment assets for the project."
    },
    "S3DeploymentRootKey": {
      "Type": "String",
      "Description": "An S3 key relative to the S3DeploymentBucket that points to the root\nof the deployment directory."
    }
  },
  "Resources": {
    "CreateAaasAndBbbsResolver": {  //変更
      "Type": "AWS::AppSync::Resolver",
      "Properties": {
        "ApiId": {
          "Ref": "AppSyncApiId"
        },
        "DataSourceName": "AaaTable",
        "TypeName": "Mutation",  //変更
        "FieldName": "createAaasAndBbbs",  //変更
        "RequestMappingTemplateS3Location": {
          "Fn::Sub": [
            "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Mutation.createAaasAndBbbs.req.vtl",  //変更
            {
              "S3DeploymentBucket": {
                "Ref": "S3DeploymentBucket"
              },
              "S3DeploymentRootKey": {
                "Ref": "S3DeploymentRootKey"
              }
            }
          ]
        },
        "ResponseMappingTemplateS3Location": {
          "Fn::Sub": [
            "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Mutation.createAaasAndBbbs.res.vtl",  //変更
            {
              "S3DeploymentBucket": {
                "Ref": "S3DeploymentBucket"
              },
              "S3DeploymentRootKey": {
                "Ref": "S3DeploymentRootKey"
              }
            }
          ]
        }
      }
    }
  },
  "Conditions": {
    "HasEnvironmentParameter": {
      "Fn::Not": [
        {
          "Fn::Equals": [
            {
              "Ref": "env"
            },
            "NONE"
          ]
        }
      ]
    },
    "AlwaysFalse": {
      "Fn::Equals": ["true", "false"]
    }
  },
  "Outputs": {
    "EmptyOutput": {
      "Description": "An empty output. You may delete this if you have at least one resource above.",
      "Value": ""
    }
  }
}

```

# リゾルバーテンプレートの作成
`./amplify/backend/api/[project]/resolvers/`に`Mutation.createAaasAndBbbs.req.vtl`と`Mutation.createAaasAndBbbs.res.vtl`を作成します。
トランザクションは`TransactWriteItems`を利用することで実現できます。
ただここですごい気になっているのが、`Aaa-xxxxxxxxxxxxxxxxxxxxxxxxxx-dev`の部分。
`-xxxxxxxxxxxxxxxxxxxxxxxxxx-dev`がDynamoDBのテーブル名に自動で振られるのだけど、vtl上で取得する方法がわかりませんでした。
このままだと別環境へのリリースとかできないんじゃ・・・。

```Mutation.createAaaAndBbb.req.vtl
#set($aaaTransactPutItems = [])
#set($index = 0)
#foreach($aaa in ${ctx.args.aaas})
  #set($keyMap = {})
  $util.qr($keyMap.put("id", $util.dynamodb.toDynamoDB($util.autoId())))
  #set($attributeValues = {})
  $util.qr($attributeValues.put("name", $util.dynamodb.toDynamoDB($aaa.name)))

  #set($index = $index + 1)
  #set($aaaTransactPutItem = {"table": "Aaa-xxxxxxxxxxxxxxxxxxxxxxxxxx-dev",
        "operation": "PutItem",
        "key": $keyMap,
        "attributeValues": $attributeValues})
  $util.qr($aaaTransactPutItems.add($aaaTransactPutItem))
#end

#set($bbbTransactPutItems = [])
#set($index = 0)
#foreach($bbb in ${ctx.args.bbbs})
  #set($keyMap = {})
  $util.qr($keyMap.put("id", $util.dynamodb.toDynamoDB($util.autoId())))
  #set($attributeValues = {})
  $util.qr($attributeValues.put("name", $util.dynamodb.toDynamoDB($bbb.name)))

  #set($index = $index + 1)
  #set($bbbTransactPutItem = {"table": "Bbb-xxxxxxxxxxxxxxxxxxxxxxxxxx-dev",
        "operation": "PutItem",
        "key": $keyMap,
        "attributeValues": $attributeValues})
  $util.qr($bbbTransactPutItems.add($bbbTransactPutItem))
#end

#set($transactItems = [])
$util.qr($transactItems.addAll($aaaTransactPutItems))
$util.qr($transactItems.addAll($bbbTransactPutItems))

{
    "version" : "2018-05-29",
    "operation" : "TransactWriteItems",
    "transactItems" : $util.toJson($transactItems)
}
```

# さいごに
上記でとりあえずトランザクション処理が実現できることまでは確認できました。
でももっとスマートな記述ができるのでは思っています。。
調査不足にもかかわらずアドベントカレンダーに手をあげてしまいましたが、この機会に自分でも上記追加調査してみようと思います。