

本文属于机器翻译版本。若本译文内容与英语原文存在差异，则一律以英文原文为准。

# 的 VTL 解析器教程 Amazon AppSync
VTL 解析器教程

**注意**  
我们现在主要支持 APPSYNC\$1JS 运行时系统及其文档。请考虑使用 APPSYNC\$1JS 运行时系统和[此处](https://docs.amazonaws.cn/appsync/latest/devguide/tutorials-js.html)的指南。

数据源和解析器用于翻译 GraphQL 请求并从 Amazon 您的资源中获取信息。 Amazon AppSync Amazon AppSync 支持自动配置和与某些数据源类型的连接。 Amazon AppSync 还支持 Amazon Lambda Amazon DynamoDB、关系数据库（亚马逊 Aurora Serverless）、 OpenSearch 亚马逊服务和 HTTP 终端节点作为数据源。您可以将 GraphQL API 与现有 Amazon 资源一起使用，也可以从头开始构建数据源和解析器。以下各节旨在以教程的形式阐明一些比较常见的 GraphQL 使用案例。

Amazon AppSync 使用以 Apache Velocity 模板语言 (VTL) 为解析器编写的*映射*模板。有关使用映射模板的更多信息，请参阅[解析器映射模板参考](resolver-mapping-template-reference.md#aws-appsync-resolver-mapping-template-reference)。有关使用 VTL 的更多信息，请参阅[解析器映射模板编程指南](resolver-mapping-template-reference-programming-guide.md#aws-appsync-resolver-mapping-template-reference-programming-guide)。

Amazon AppSync 支持从 GraphQL 架构自动配置 DynamoDB 表，如从架构置备（可选）和启动示例架构中所述。您也可以从现有 DynamoDB 表中导入，从而创建架构并连接解析器。在“从 Amazon DynamoDB 导入”（可选）中简要说明了该内容。

**Topics**
+ [

# 使用 DynamoDB 解析器创建简单的文章应用程序
](tutorial-dynamodb-resolvers.md)
+ [使用 Amazon Lambda 解析器](tutorial-lambda-resolvers.md)
+ [使用 OpenSearch 服务解析器](tutorial-elasticsearch-resolvers.md)
+ [使用本地解析器](tutorial-local-resolvers.md)
+ [组合使用 GraphQL 解析器](tutorial-combining-graphql-resolvers.md)
+ [执行 DynamoDB 批处理操作](tutorial-dynamodb-batch.md)
+ [执行 DynamoDB 事务](tutorial-dynamodb-transact.md)
+ [使用 HTTP 解析器](tutorial-http-resolvers.md)
+ [使用 Aurora Serverless v2 解析器](tutorial-rds-resolvers.md)
+ [使用管线解析器](tutorial-pipeline-resolvers.md)
+ [对版本控制的数据来源执行增量同步操作](tutorial-delta-sync.md)

# 使用 DynamoDB 解析器创建简单的文章应用程序


**注意**  
我们现在主要支持 APPSYNC\$1JS 运行时系统及其文档。请考虑使用 APPSYNC\$1JS 运行时系统和[此处](https://docs.amazonaws.cn/appsync/latest/devguide/tutorials-js.html)的指南。

本教程展示了如何将自己的亚马逊 DynamoDB 表带到 GraphQL API 并将其连接到 Amazon AppSync GraphQL API。

您可以让代表您 Amazon AppSync 配置 DynamoDB 资源。如果您愿意，也可以创建数据来源和解析器，将现有的表连接到 GraphQL 架构。在这两种情况下，您都可以通过 GraphQL 语句读写您的 DynamoDB 数据库，并订阅实时数据。

要将 GraphQL 语句转换为 DynamoDB 操作，并将响应转换回 GraphQL，需要完成一些特定的配置步骤。本教程通过一些现实世界的场景和数据访问模式介绍了配置过程。

## 设置您的 DynamoDB 表


要开始本教程，首先需要按照以下步骤配置 Amazon 资源。

1. 在 CLI 中使用以下 Amazon CloudFormation 模板配置 Amazon 资源：

   ```
   aws cloudformation create-stack \
       --stack-name AWSAppSyncTutorialForAmazonDynamoDB \
       --template-url https://s3.us-west-2.amazonaws.com/awsappsync/resources/dynamodb/AmazonDynamoDBCFTemplate.yaml \
       --capabilities CAPABILITY_NAMED_IAM
   ```

   或者，您可以在自己的 Amazon 账户中在美国西部 2（俄勒冈）地区启动以下 Amazon CloudFormation 堆栈。

   [https://console.amazonaws.cn/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3.us-west-2.amazonaws.com/awsappsync/resources/dynamodb/AmazonDynamoDBCFTemplate.yaml](https://console.amazonaws.cn/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3.us-west-2.amazonaws.com/awsappsync/resources/dynamodb/AmazonDynamoDBCFTemplate.yaml)

   这会创建以下内容：
   + 名为 `AppSyncTutorial-Post` 的 DynamoDB 表，用于保留 `Post` 数据。
   + 允许 Amazon AppSync 与`Post`表交互的 IAM 角色和关联的 IAM 托管策略。

1. 要了解堆栈和所创建资源的更多详细信息，请运行以下 CLI 命令：

   ```
   aws cloudformation describe-stacks --stack-name AWSAppSyncTutorialForAmazonDynamoDB
   ```

1. 稍后要删除资源，您可以运行以下操作：

   ```
   aws cloudformation delete-stack --stack-name AWSAppSyncTutorialForAmazonDynamoDB
   ```

## 创建您的 GraphQL API


要在以下位置创建 GraphQL API，请执行以下操作： Amazon AppSync

1. 登录 Amazon Web Services 管理控制台 并打开[AppSync 控制台](https://console.amazonaws.cn/appsync/)。

   1. 在**APIs 控制面板**中，选择**创建 API**。

1. 在**自定义您的 API 或从 Amazon DynamoDB 导入**下面，选择**从头开始构建**。

   1. 选择同一窗口右侧的**开始**。

1. 在 **API 名称**字段中，将 API 的名称设置为 `AWSAppSyncTutorial`。

1. 选择**创建**。

 Amazon AppSync 控制台使用 API 密钥身份验证模式为您创建一个新的 GraphQL API。您可以根据本教程后面的说明，使用控制台设置 GraphQL API 的其余部分，并针对它运行查询。

## 定义基本文章 API


现在，您已经创建了 Amazon AppSync GraphQL API，可以设置一个允许基本创建、检索和删除帖子数据的基本架构。

1. 登录 Amazon Web Services 管理控制台 并打开[AppSync 控制台](https://console.amazonaws.cn/appsync/)。

   1. 在**APIs 控制面板**中，选择您刚刚创建的 API。

1. 在**侧边栏**中，选择**架构**。

   1. 在**架构**窗格中，将内容替换为以下代码：

     ```
     schema {
         query: Query
         mutation: Mutation
     }
     
     type Query {
         getPost(id: ID): Post
     }
     
     type Mutation {
         addPost(
             id: ID!
             author: String!
             title: String!
             content: String!
             url: String!
         ): Post!
     }
     
     type Post {
         id: ID!
         author: String
         title: String
         content: String
         url: String
         ups: Int!
         downs: Int!
         version: Int!
     }
     ```

1. 选择**保存**。

此架构定义 `Post` 类型，执行操作以添加并获取 `Post` 对象。

## 为 DynamoDB 表配置数据来源


接下来，将架构中定义的查询和变更链接到 `AppSyncTutorial-Post` DynamoDB 表。

首先， Amazon AppSync 需要注意你的表格。要实现此目的，需要在 Amazon AppSync 中设置数据源：

1. 登录 Amazon Web Services 管理控制台 并打开[AppSync 控制台](https://console.amazonaws.cn/appsync/)。

   1. 在**APIs 控制面板**中，选择你的 GraphQL API。

   1. 在**侧边栏**中，选择**数据来源**。

1. 选择**创建数据来源**。

   1. 对于**数据来源名称**，输入 `PostDynamoDBTable`。

   1. 对于**数据来源类型**，选择 **Amazon DynamoDB 表**。

   1. 对于**区域**，选择 **US-WEST-2**。

   1. 对于**表名**，选择 **AppSyncTutorial-Pos** t DynamoDB 表。

   1. 创建新的 IAM 角色（建议），或者选择具有 `lambda:invokeFunction` IAM 权限的现有角色。现有角色需要具有一个信任策略，如[附加数据来源](attaching-a-data-source.md)一节中所述。

      以下是一个示例 IAM 策略，该策略具有对资源执行操作所需的权限：

------
#### [ JSON ]

****  

      ```
      { 
           "Version":"2012-10-17",		 	 	  
           "Statement": [ 
               { 
                   "Effect": "Allow", 
                   "Action": [ "lambda:invokeFunction" ], 
                   "Resource": [ 
                       "arn:aws:lambda:us-east-1:111122223333:function:myFunction", 
                       "arn:aws:lambda:us-east-1:111122223333:function:myFunction:*" 
                   ] 
               } 
           ] 
       }
      ```

------

1. 选择**创建**。

## 设置 AddPost 解析器 (DynamoDB) PutItem


**在知道 DynamoDB 表之后 Amazon AppSync ，您可以通过定义解析器将其链接到各个查询和变更。**您创建的第一个解析器是 `addPost` 解析器，可用于在 `AppSyncTutorial-Post` DynamoDB 表中创建文章。

解析器具有以下组件：
+ GraphQL 架构中的位置，用于附加解析器。在本例中，您将设置 `addPost` 类型的 `Mutation` 字段的解析器。在调用方调用 `mutation { addPost(...){...} }` 时，将调用该解析器。
+ 此解析器所用的数据来源。在本例中，您要使用之前定义的 `PostDynamoDBTable` 数据来源，这样您就可以在 `AppSyncTutorial-Post` DynamoDB 表中添加条目。
+ 请求映射模板。请求映射模板的目的是接收来自调用者的传入请求，并将其转换为要对 DynamoDB 执行的 Amazon AppSync 指令。
+ 响应映射模板。响应映射模板的任务是将 DynamoDB 的响应转换回 GraphQL 期待获得的内容。如果 DynamoDB 中的数据形态与 GraphQL 中的 `Post` 类型不同，此模板很有用。但在此例中它们的形态相同，所以只用于传递数据。

设置解析器：

1. 登录 Amazon Web Services 管理控制台 并打开[AppSync 控制台](https://console.amazonaws.cn/appsync/)。

   1. 在**APIs 控制面板**中，选择你的 GraphQL API。

   1. 在**侧边栏**中，选择**数据来源**。

1. 选择**创建数据来源**。

   1. 对于**数据来源名称**，输入 `PostDynamoDBTable`。

   1. 对于**数据来源类型**，选择 **Amazon DynamoDB 表**。

   1. 对于**区域**，选择 **US-WEST-2**。

   1. 对于**表名**，选择 **AppSyncTutorial-Pos** t DynamoDB 表。

   1. 创建新的 IAM 角色（建议），或者选择具有 `lambda:invokeFunction` IAM 权限的现有角色。现有角色需要具有一个信任策略，如[附加数据来源](attaching-a-data-source.md)一节中所述。

      以下是一个示例 IAM 策略，该策略具有对资源执行操作所需的权限：

------
#### [ JSON ]

****  

      ```
      { 
           "Version":"2012-10-17",		 	 	  
           "Statement": [ 
               { 
                   "Effect": "Allow", 
                   "Action": [ "lambda:invokeFunction" ], 
                   "Resource": [ 
                       "arn:aws:lambda:us-west-2:123456789012:function:myFunction", 
                       "arn:aws:lambda:us-west-2:123456789012:function:myFunction:*" 
                   ] 
               } 
           ] 
       }
      ```

------

1. 选择**创建**。

1. 选择**架构**选项卡。

1. 在右侧的**数据类型**窗格中，找到 **Mutation** 类型上的 **addPost** 字段，然后选择**附加**。

1. 在**操作**菜单中，选择**更新运行时**，然后选择**单位解析器 (仅限 VTL)**。

1. 在 **Data source name (数据源名称)** 中，选择 **PostDynamoDBTable**。

1. 在 **Configure the request mapping template (配置请求映射模板)** 中，粘贴以下内容：

   ```
   {
       "version" : "2017-02-28",
       "operation" : "PutItem",
       "key" : {
           "id" : $util.dynamodb.toDynamoDBJson($context.arguments.id)
       },
       "attributeValues" : {
           "author" : $util.dynamodb.toDynamoDBJson($context.arguments.author),
           "title" : $util.dynamodb.toDynamoDBJson($context.arguments.title),
           "content" : $util.dynamodb.toDynamoDBJson($context.arguments.content),
           "url" : $util.dynamodb.toDynamoDBJson($context.arguments.url),
           "ups" : { "N" : 1 },
           "downs" : { "N" : 0 },
           "version" : { "N" : 1 }
       }
   }
   ```

   **注意**：为所有键和属性值指定了*类型*。例如，您将 `author` 字段设置为 `{ "S" : "${context.arguments.author}" }`。该`S`部分向 DynamoDB 表示该值将是一个字符串值。 Amazon AppSync 实际的值由 `author` 参数填充。与此类似，`version` 字段是一个数字字段，因为它使用 `N` 作为类型。最后，您还将初始化 `ups`、`downs` 和 `version` 字段。

   在本教程中，您已指定 GraphQL `ID!` 类型（用于索引插入到 DynamoDB 的新项目）作为客户端参数的一部分。 Amazon AppSync 附带了一个名为的自动生成身份的实用程序`$utils.autoId()`，您也可以以以下形式使用该实用程序`"id" : { "S" : "${$utils.autoId()}" }`。然后，就可以在 `id: ID!` 的架构定义中省去 `addPost()`，因为它将自动插入。您不会在本教程中使用该技术，但在写入到 DynamoDB 表时，您应该将其视为一种很好的做法。

   有关映射模板的更多信息，请参阅 [解析器映射模板概述](resolver-mapping-template-reference-overview.md#aws-appsync-resolver-mapping-template-reference-overview)参考文档。有关 GetItem 请求映射的更多信息，请参阅[GetItem](aws-appsync-resolver-mapping-template-reference-dynamodb-getitem.md)参考文档。有关类型的更多信息，请参阅[类型系统（请求映射）](aws-appsync-resolver-mapping-template-reference-dynamodb-typed-values-request.md)参考文档。

1. 在 **Configure the response mapping template (配置响应映射模板)** 中，粘贴以下内容：

   ```
   $utils.toJson($context.result)
   ```

    **注意**：由于 `AppSyncTutorial-Post` 表中的数据形态与 GraphQL 中 `Post` 类型的形态完全匹配，响应映射模板只会直接传递结果。还请注意，此教程中的所有示例均使用同一响应映射模板，所以您只需创建一个文件。

1. 选择**保存**。

### 调用 API 以添加文章


现在，解析器已设置完毕， Amazon AppSync 可以将传入的`addPost`突变转换为 DynamoDB 操作。 PutItem 现在，您可以运行一个变更，在表中添加内容。
+ 选择 **Queries** 选项卡。
+ 在 **Queries (查询)** 窗格中，粘贴以下变更：

  ```
  mutation addPost {
    addPost(
      id: 123
      author: "AUTHORNAME"
      title: "Our first post!"
      content: "This is our first post."
      url: "https://aws.amazon.com/appsync/"
    ) {
      id
      author
      title
      content
      url
      ups
      downs
      version
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。
+ 新创建的文章的结果应出现在查询窗格右侧的结果窗格中。如下所示：

  ```
  {
    "data": {
      "addPost": {
        "id": "123",
        "author": "AUTHORNAME",
        "title": "Our first post!",
        "content": "This is our first post.",
        "url": "https://aws.amazon.com/appsync/",
        "ups": 1,
        "downs": 0,
        "version": 1
      }
    }
  }
  ```

以下是具体过程：
+ Amazon AppSync 收到了`addPost`变异请求。
+ Amazon AppSync 接受了请求和请求映射模板，并生成了请求映射文档。如下所示：

  ```
  {
      "version" : "2017-02-28",
      "operation" : "PutItem",
      "key" : {
          "id" : { "S" : "123" }
      },
      "attributeValues" : {
          "author": { "S" : "AUTHORNAME" },
          "title": { "S" : "Our first post!" },
          "content": { "S" : "This is our first post." },
          "url": { "S" : "https://aws.amazon.com/appsync/" },
          "ups" : { "N" : 1 },
          "downs" : { "N" : 0 },
          "version" : { "N" : 1 }
      }
  }
  ```
+ Amazon AppSync 使用请求映射文档生成并执行 DynamoDB `PutItem` 请求。
+ Amazon AppSync 获取`PutItem`请求的结果并将其转换回 GraphQL 类型。

  ```
  {
      "id" : "123",
      "author": "AUTHORNAME",
      "title": "Our first post!",
      "content": "This is our first post.",
      "url": "https://aws.amazon.com/appsync/",
      "ups" : 1,
      "downs" : 0,
      "version" : 1
  }
  ```
+ 通过响应映射文档进行传递，没有变化。
+ 在 GraphQL 响应中返回新创建的对象。

## 设置 GetPost 解析器 (DynamoDB) GetItem


您现在能够将数据添加到 `AppSyncTutorial-Post` DynamoDB 表中，您需要设置 `getPost` 查询，以使其可以从 `AppSyncTutorial-Post` 表中检索该数据。为了实现此目的，您要设置另一解析器。
+ 选择**架构**选项卡。
+ 在右侧的**数据类型**窗格中，找到 **Query** 类型上的 **getPost** 字段，然后选择**附加**。
+ 在**操作**菜单中，选择**更新运行时**，然后选择**单位解析器 (仅限 VTL)**。
+ 在 **Data source name (数据源名称)** 中，选择 **PostDynamoDBTable**。
+ 在 **Configure the request mapping template (配置请求映射模板)** 中，粘贴以下内容：

  ```
  {
      "version" : "2017-02-28",
      "operation" : "GetItem",
      "key" : {
          "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id)
      }
  }
  ```
+ 在 **Configure the response mapping template (配置响应映射模板)** 中，粘贴以下内容：

  ```
  $utils.toJson($context.result)
  ```
+ 选择**保存**。

### 调用 API 以获取文章


现在，解析器已经设置完毕， Amazon AppSync 知道如何将传入的`getPost`查询转换为 DynamoDB `GetItem` 操作。现在，您可以运行查询，检索之前创建的文章。
+ 选择 **Queries** 选项卡。
+ 在**Queries (查询)** 窗格中粘贴以下内容：

  ```
  query getPost {
    getPost(id:123) {
      id
      author
      title
      content
      url
      ups
      downs
      version
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。
+ 从 DynamoDB 中检索的文章应显示在查询窗格右侧的结果窗格中。如下所示：

  ```
  {
    "data": {
      "getPost": {
        "id": "123",
        "author": "AUTHORNAME",
        "title": "Our first post!",
        "content": "This is our first post.",
        "url": "https://aws.amazon.com/appsync/",
        "ups": 1,
        "downs": 0,
        "version": 1
      }
    }
  }
  ```

以下是具体过程：
+ Amazon AppSync 收到了`getPost`查询请求。
+ Amazon AppSync 接受了请求和请求映射模板，并生成了请求映射文档。如下所示：

  ```
  {
      "version" : "2017-02-28",
      "operation" : "GetItem",
      "key" : {
          "id" : { "S" : "123" }
      }
  }
  ```
+ Amazon AppSync 使用请求映射文档生成并执行 DynamoDB GetItem 请求。
+ Amazon AppSync 获取`GetItem`请求的结果并将其转换回 GraphQL 类型。

  ```
  {
      "id" : "123",
      "author": "AUTHORNAME",
      "title": "Our first post!",
      "content": "This is our first post.",
      "url": "https://aws.amazon.com/appsync/",
      "ups" : 1,
      "downs" : 0,
      "version" : 1
  }
  ```
+ 通过响应映射文档进行传递，没有变化。
+ 在响应中返回检索到的对象。

或者，采用以下示例：

```
query getPost {
  getPost(id:123) {
    id
    author
    title
  }
}
```

如果您的 `getPost` 查询仅需要 `id`、`author` 和 `title`，您可以将请求映射模板更改为使用投影表达式仅指定您希望从 DynamoDB 表中获取的属性，以避免将不必要的数据从 DynamoDB 传输到 Amazon AppSync。例如，请求映射模板可能类似于以下代码片段：

```
{
    "version" : "2017-02-28",
    "operation" : "GetItem",
    "key" : {
        "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id)
    },
    "projection" : {
     "expression" : "#author, id, title",
     "expressionNames" : { "#author" : "author"}
    }
}
```

## 创建 UpdatePost 突变 (DynamoDB) UpdateItem


到目前为止，您可以在 DynamoDB 中创建和检索 `Post` 对象。现在，您要设置一项新的变更，以便更新对象。您将使用 UpdateItem DynamoDB 操作来执行此操作。
+ 选择**架构**选项卡。
+ 在 **Schema (架构)** 窗格中修改 `Mutation` 类型，添加新的 `updatePost` 变更，如下所示：

  ```
  type Mutation {
      updatePost(
          id: ID!,
          author: String!,
          title: String!,
          content: String!,
          url: String!
      ): Post
      addPost(
          author: String!
          title: String!
          content: String!
          url: String!
      ): Post!
  }
  ```
+ 选择**保存**。
+ 在右侧的**数据类型**窗格中，找到 **Mutation** 类型上的新创建的 **updatePost** 字段，然后选择**附加**。
+ 在**操作**菜单中，选择**更新运行时**，然后选择**单位解析器 (仅限 VTL)**。
+ 在 **Data source name (数据源名称)** 中，选择 **PostDynamoDBTable**。
+ 在 **Configure the request mapping template (配置请求映射模板)** 中，粘贴以下内容：

  ```
  {
      "version" : "2017-02-28",
      "operation" : "UpdateItem",
      "key" : {
          "id" : $util.dynamodb.toDynamoDBJson($context.arguments.id)
      },
      "update" : {
          "expression" : "SET author = :author, title = :title, content = :content, #url = :url ADD version :one",
          "expressionNames": {
              "#url" : "url"
          },
          "expressionValues": {
              ":author" : $util.dynamodb.toDynamoDBJson($context.arguments.author),
              ":title" : $util.dynamodb.toDynamoDBJson($context.arguments.title),
              ":content" : $util.dynamodb.toDynamoDBJson($context.arguments.content),
              ":url" : $util.dynamodb.toDynamoDBJson($context.arguments.url),
              ":one" : { "N": 1 }
          }
      }
  }
  ```

   **注意：**此解析器使用的是 D UpdateItem ynamoDB，这与操作有很大不同。 PutItem 您仅要求 DynamoDB 更新某些属性，而不是编写整个项目。这是使用 DynamoDB 更新表达式完成的。表达式本身是在 `expression` 部分的 `update` 字段中指定的。它会设置 `author`、`title`、`content` 和 URL 属性，还会递增 `version` 字段。要使用的值不会出现在表达式本身；表达式中的占位符名称以冒号打头，并在 `expressionValues` 字段中进行定义。最后，DynamoDB 具有一些保留字，它们不能出现在 `expression` 中。例如，`url` 是保留关键字，所以要更新 `url` 字段，您可使用名称占位符，并在 `expressionNames` 字段中定义它们。

  有关`UpdateItem`请求映射的更多信息，请参阅[UpdateItem](aws-appsync-resolver-mapping-template-reference-dynamodb-updateitem.md)参考文档。有关如何编写更新表达式的更多信息，请参阅 [DynamoDB 文档 UpdateExpressions ](https://docs.amazonaws.cn/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html)。
+ 在 **Configure the response mapping template (配置响应映射模板)** 中，粘贴以下内容：

  ```
  $utils.toJson($context.result)
  ```

### 调用 API 以更新文章


现在解析器已经设置好了， Amazon AppSync 知道如何将传入的`update`突变转换为 DynamoDB 操作。`Update`现在，您可以运行变更，以更新您之前写入的项目。
+ 选择 **Queries** 选项卡。
+ 在 **Queries (查询)** 窗格中，粘贴以下变更。您还需要将 `id` 参数更新为您以前记下的值。

  ```
  mutation updatePost {
    updatePost(
      id:"123"
      author: "A new author"
      title: "An updated author!"
      content: "Now with updated content!"
      url: "https://aws.amazon.com/appsync/"
    ) {
      id
      author
      title
      content
      url
      ups
      downs
      version
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。
+ 在 DynamoDB 中更新的文章应显示在查询窗格右侧的结果窗格中。如下所示：

  ```
  {
    "data": {
      "updatePost": {
        "id": "123",
        "author": "A new author",
        "title": "An updated author!",
        "content": "Now with updated content!",
        "url": "https://aws.amazon.com/appsync/",
        "ups": 1,
        "downs": 0,
        "version": 2
      }
    }
  }
  ```

在此示例中，没有修改`ups`和`downs`字段，因为请求映射模板没有要求 Amazon AppSync 和 DynamoDB 对这些字段执行任何操作。此外，该`version`字段增加了 1，因为您要求 Amazon AppSync 和 DynamoDB 向该字段添加 1。`version`

## 修改 UpdatePost 解析器 (DynamoDB) UpdateItem


`updatePost` 变更看上去不错，但它有两个主要问题：
+ 如果您只希望更新一个字段，则必须更新所有字段。
+ 如果两个人同时修改对象，您可能会丢失信息。

为了解决这些问题，您要修改 `updatePost` 变更，做到只修改请求中指定的参数，然后在 `UpdateItem` 操作中添加条件。

1. 选择**架构**选项卡。

1. 在**架构**窗格中修改 `Mutation` 类型中的 `updatePost` 字段，删除 `author`、`title`、`content` 和 `url` 参数的感叹号，确保 `id` 字段不变。这样它们就会成为可选参数。还要新增一个必需 `expectedVersion` 参数。

   ```
   type Mutation {
       updatePost(
           id: ID!,
           author: String,
           title: String,
           content: String,
           url: String,
           expectedVersion: Int!
       ): Post
       addPost(
           author: String!
           title: String!
           content: String!
           url: String!
       ): Post!
   }
   ```

1. 选择**保存**。

1. 在右侧的**数据类型**窗格中，找到 **Mutation** 类型的 **updatePost** 字段。

1. 选择**PostDynamoDBTable**打开现有的解析器。

1. 在 **Configure the request mapping template (配置请求映射模板)** 中修改请求映射模板，如下所示：

   ```
   {
       "version" : "2017-02-28",
       "operation" : "UpdateItem",
       "key" : {
           "id" : $util.dynamodb.toDynamoDBJson($context.arguments.id)
       },
   
       ## Set up some space to keep track of things you're updating **
       #set( $expNames  = {} )
       #set( $expValues = {} )
       #set( $expSet = {} )
       #set( $expAdd = {} )
       #set( $expRemove = [] )
   
       ## Increment "version" by 1 **
       $!{expAdd.put("version", ":one")}
       $!{expValues.put(":one", { "N" : 1 })}
   
       ## Iterate through each argument, skipping "id" and "expectedVersion" **
       #foreach( $entry in $context.arguments.entrySet() )
           #if( $entry.key != "id" && $entry.key != "expectedVersion" )
               #if( (!$entry.value) && ("$!{entry.value}" == "") )
                   ## If the argument is set to "null", then remove that attribute from the item in DynamoDB **
   
                   #set( $discard = ${expRemove.add("#${entry.key}")} )
                   $!{expNames.put("#${entry.key}", "$entry.key")}
               #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}", { "S" : "${entry.value}" })}
               #end
           #end
       #end
   
       ## Start building the update expression, starting with attributes you're going to SET **
       #set( $expression = "" )
       #if( !${expSet.isEmpty()} )
           #set( $expression = "SET" )
           #foreach( $entry in $expSet.entrySet() )
               #set( $expression = "${expression} ${entry.key} = ${entry.value}" )
               #if ( $foreach.hasNext )
                   #set( $expression = "${expression}," )
               #end
           #end
       #end
   
       ## Continue building the update expression, adding attributes you're going to ADD **
       #if( !${expAdd.isEmpty()} )
           #set( $expression = "${expression} ADD" )
           #foreach( $entry in $expAdd.entrySet() )
               #set( $expression = "${expression} ${entry.key} ${entry.value}" )
               #if ( $foreach.hasNext )
                   #set( $expression = "${expression}," )
               #end
           #end
       #end
   
       ## Continue building the update expression, adding attributes you're going to REMOVE **
       #if( !${expRemove.isEmpty()} )
           #set( $expression = "${expression} REMOVE" )
   
           #foreach( $entry in $expRemove )
               #set( $expression = "${expression} ${entry}" )
               #if ( $foreach.hasNext )
                   #set( $expression = "${expression}," )
               #end
           #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
       },
   
       "condition" : {
           "expression"       : "version = :expectedVersion",
           "expressionValues" : {
               ":expectedVersion" : $util.dynamodb.toDynamoDBJson($context.arguments.expectedVersion)
           }
       }
   }
   ```

1. 选择**保存**。

此模板是一个更复杂的示例。它演示了映射模板的强大功能和灵活性。它遍历所有参数，跳过 `id` 和 `expectedVersion`。如果参数设置为某个值，它会要求 Amazon AppSync 和 DynamoDB 在 DynamoDB 中更新该对象上的该属性。如果该属性设置为空，它会要求 Amazon AppSync 和 DynamoDB 从帖子对象中移除该属性。如果未指定参数，该属性会保留原样。它还会递增 `version` 字段。

还有一个新的 `condition` 部分。条件表达式允许您根据执行 Amazon AppSync 操作之前已在 DynamoDB 中的对象的状态告知和 DynamoDB 请求是否应该成功。在该示例中，只有在当前位于 DynamoDB 中的项目的 `version` 字段与 `expectedVersion` 参数完全匹配时，您才希望 `UpdateItem` 请求成功。

有关条件表达式的更多信息，请参阅[条件表达式](aws-appsync-resolver-mapping-template-reference-dynamodb-condition-expressions.md)参考文档。

### 调用 API 以更新文章


让我们尝试使用新的解析器更新 `Post` 对象：
+ 选择 **Queries** 选项卡。
+ 在 **Queries (查询)** 窗格中，粘贴以下更改。您还需要将 `id` 参数更新为您以前记下的值。

  ```
  mutation updatePost {
    updatePost(
      id:123
      title: "An empty story"
      content: null
      expectedVersion: 2
    ) {
      id
      author
      title
      content
      url
      ups
      downs
      version
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。
+ 在 DynamoDB 中更新的文章应显示在查询窗格右侧的结果窗格中。如下所示：

  ```
  {
    "data": {
      "updatePost": {
        "id": "123",
        "author": "A new author",
        "title": "An empty story",
        "content": null,
        "url": "https://aws.amazon.com/appsync/",
        "ups": 1,
        "downs": 0,
        "version": 3
      }
    }
  }
  ```

在此请求中，您要求 Amazon AppSync 和 DynamoDB 仅更新和字段`title`。`content`它不会处理所有其他字段（除了递增 `version` 字段）。您将 `title` 属性设置为新的值，并从文章中删除 `content` 属性。`author`、`url`、`ups` 和 `downs` 字段没有变化。

请尝试再次执行变更请求，保持请求完全不变。您可以看到类似以下内容的响应：

```
{
  "data": {
    "updatePost": null
  },
  "errors": [
    {
      "path": [
        "updatePost"
      ],
      "data": {
        "id": "123",
        "author": "A new author",
        "title": "An empty story",
        "content": null,
        "url": "https://aws.amazon.com/appsync/",
        "ups": 1,
        "downs": 0,
        "version": 3
      },
      "errorType": "DynamoDB:ConditionalCheckFailedException",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "message": "The conditional request failed (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException; Request ID: ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ)"
    }
  ]
}
```

请求失败，因为条件表达式的评估结果为 false：
+ 第一次运行请求时，DynamoDB 中的文章的 `version` 字段的值为 `2`，它与 `expectedVersion` 参数匹配。请求成功，这意味着 DynamoDB 中的 `version` 字段已增加到 `3`。
+ 第二次运行请求时，DynamoDB 中的文章的 `version` 字段的值为 `3`，它与 `expectedVersion` 参数不匹配。

这种模式通常被称为*乐观锁*。

 Amazon AppSync DynamoDB 解析器的一个特点是它返回 DynamoDB 中帖子对象的当前值。您可以在 GraphQL 响应的 `data` 部分的 `errors` 字段中找到这个值。您的应用程序可以利用此信息决定应如何继续。在该示例中，您可以看到 DynamoDB 中的对象的 `version` 字段设置为 `3`，因此，您只需将 `expectedVersion` 参数更新为 `3`，请求就会再次成功。

有关如何处理条件检查失败的更多信息，请参阅[条件表达式](aws-appsync-resolver-mapping-template-reference-dynamodb-condition-expressions.md)映射模板参考文档。

## 创建 upvotePost 和 downvotePost 突变 (DynamoDB) UpdateItem


`Post` 类型有 `ups` 和 `downs` 字段，用于记录点赞和差评，但现在还无法通过 API 使用它们。让我们添加一些变更，对文章点赞和差评。
+ 选择**架构**选项卡。
+ 在 **Schema (架构)** 窗格中修改 `Mutation` 类型，添加新的 `upvotePost` 和 `downvotePost` 变更，如下所示：

  ```
  type Mutation {
      upvotePost(id: ID!): Post
      downvotePost(id: ID!): Post
      updatePost(
          id: ID!,
          author: String,
          title: String,
          content: String,
          url: String,
          expectedVersion: Int!
      ): Post
      addPost(
          author: String!,
          title: String!,
          content: String!,
          url: String!
      ): Post!
  }
  ```
+ 选择**保存**。
+ 在右侧的**数据类型**窗格中，找到 **Mutation** 类型上的新创建的 **upvotePost** 字段，然后选择**附加**。
+ 在**操作**菜单中，选择**更新运行时**，然后选择**单位解析器 (仅限 VTL)**。
+ 在 **Data source name (数据源名称)** 中，选择 **PostDynamoDBTable**。
+ 在 **Configure the request mapping template (配置请求映射模板)** 中，粘贴以下内容：

  ```
  {
      "version" : "2017-02-28",
      "operation" : "UpdateItem",
      "key" : {
          "id" : $util.dynamodb.toDynamoDBJson($context.arguments.id)
      },
      "update" : {
          "expression" : "ADD ups :plusOne, version :plusOne",
          "expressionValues" : {
              ":plusOne" : { "N" : 1 }
          }
      }
  }
  ```
+ 在 **Configure the response mapping template (配置响应映射模板)** 中，粘贴以下内容：

  ```
  $utils.toJson($context.result)
  ```
+ 选择**保存**。
+ 在右侧的**数据类型**窗格中，找到 **Mutation** 类型上的新创建的 `downvotePost` 字段，然后选择**附加**。
+ 在 **Data source name (数据源名称)** 中，选择 **PostDynamoDBTable**。
+ 在 **Configure the request mapping template (配置请求映射模板)** 中，粘贴以下内容：

  ```
  {
      "version" : "2017-02-28",
      "operation" : "UpdateItem",
      "key" : {
          "id" : $util.dynamodb.toDynamoDBJson($context.arguments.id)
      },
      "update" : {
          "expression" : "ADD downs :plusOne, version :plusOne",
          "expressionValues" : {
              ":plusOne" : { "N" : 1 }
          }
      }
  }
  ```
+ 在 **Configure the response mapping template (配置响应映射模板)** 中，粘贴以下内容：

  ```
  $utils.toJson($context.result)
  ```
+ 选择**保存**。

### 调用 API，为文章点赞或差评


现在，新的解析器已经设置完毕， Amazon AppSync 知道如何将传入`upvotePost`或`downvote`突变转换为 DynamoDB 操作。 UpdateItem 现在您可以运行变更，为之前创建的文章点赞或差评。
+ 选择 **Queries** 选项卡。
+ 在 **Queries (查询)** 窗格中，粘贴以下更改。您还需要将 `id` 参数更新为您以前记下的值。

  ```
  mutation votePost {
    upvotePost(id:123) {
      id
      author
      title
      content
      url
      ups
      downs
      version
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。
+ 将在 DynamoDB 中更新文章，并且应显示在查询窗格右侧的结果窗格中。如下所示：

  ```
  {
    "data": {
      "upvotePost": {
        "id": "123",
        "author": "A new author",
        "title": "An empty story",
        "content": null,
        "url": "https://aws.amazon.com/appsync/",
        "ups": 6,
        "downs": 0,
        "version": 4
      }
    }
  }
  ```
+ 再选择几次 ** (执行查询)** 按钮。您应看到，每次您执行查询时，`ups` 和 `version` 字段均会递增 1。
+ 更改查询以调用 `downvotePost` 变更，如下所示：

  ```
  mutation votePost {
    downvotePost(id:123) {
      id
      author
      title
      content
      url
      ups
      downs
      version
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。这次您应看到，每次您执行查询时，`downs` 和 `version` 字段均会递增 1。

  ```
  {
    "data": {
      "downvotePost": {
        "id": "123",
        "author": "A new author",
        "title": "An empty story",
        "content": null,
        "url": "https://aws.amazon.com/appsync/",
        "ups": 6,
        "downs": 4,
        "version": 12
      }
    }
  }
  ```

## 设置 DeletePost 解析器 (DynamoDB) DeleteItem


接下来您要设置的变更是删除一个文章。您将使用 `DeleteItem` DynamoDB 操作完成该操作。
+ 选择**架构**选项卡。
+ 在 **Schema (架构)** 窗格中修改 `Mutation` 类型，添加新的 `deletePost` 变更，如下所示：

  ```
  type Mutation {
      deletePost(id: ID!, expectedVersion: Int): Post
      upvotePost(id: ID!): Post
      downvotePost(id: ID!): Post
      updatePost(
          id: ID!,
          author: String,
          title: String,
          content: String,
          url: String,
          expectedVersion: Int!
      ): Post
      addPost(
          author: String!,
          title: String!,
          content: String!,
          url: String!
      ): Post!
  }
  ```

  这次您将 `expectedVersion` 字段设为可选，稍后在添加请求映射模板时将对此进行说明。
+ 选择**保存**。
+ 在右侧的**数据类型**窗格中，找到 **Mutation** 类型上的新创建的 **delete** 字段，然后选择**附加**。
+ 在**操作**菜单中，选择**更新运行时**，然后选择**单位解析器 (仅限 VTL)**。
+ 在 **Data source name (数据源名称)** 中，选择 **PostDynamoDBTable**。
+ 在 **Configure the request mapping template (配置请求映射模板)** 中，粘贴以下内容：

  ```
  {
      "version" : "2017-02-28",
      "operation" : "DeleteItem",
      "key": {
          "id": $util.dynamodb.toDynamoDBJson($context.arguments.id)
      }
      #if( $context.arguments.containsKey("expectedVersion") )
          ,"condition" : {
              "expression"       : "attribute_not_exists(id) OR version = :expectedVersion",
              "expressionValues" : {
                  ":expectedVersion" : $util.dynamodb.toDynamoDBJson($context.arguments.expectedVersion)
              }
          }
      #end
  }
  ```

   **注意**：`expectedVersion` 参数是可选的。如果调用方在请求中设置 `expectedVersion` 参数，模板将添加一个条件，只有在已删除项目或 DynamoDB 中的文章的 `version` 属性与 `expectedVersion` 完全匹配时，才允许 `DeleteItem` 请求成功。如果未设置此参数，则 `DeleteItem` 请求中不指定条件表达式。无论 `version` 值如何，或者项目在 DynamoDB 中是否存在，该请求都会成功。
+ 在 **Configure the response mapping template (配置响应映射模板)** 中，粘贴以下内容：

  ```
  $utils.toJson($context.result)
  ```

   **注意**：即使您要删除一个项目，如果该项目不是已经删除，还是可以返回要删除的项目。
+ 选择**保存**。

有关`DeleteItem`请求映射的更多信息，请参阅[DeleteItem](aws-appsync-resolver-mapping-template-reference-dynamodb-deleteitem.md)参考文档。

### 调用 API 以删除文章


现在解析器已经设置好了， Amazon AppSync 知道如何将传入的`delete`突变转换为 DynamoDB 操作。`DeleteItem`现在，您可以运行变更，从表中删除一些内容。
+ 选择 **Queries** 选项卡。
+ 在 **Queries (查询)** 窗格中，粘贴以下更改。您还需要将 `id` 参数更新为您以前记下的值。

  ```
  mutation deletePost {
    deletePost(id:123) {
      id
      author
      title
      content
      url
      ups
      downs
      version
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。
+ 该文章已从 DynamoDB 中删除。请注意， Amazon AppSync 返回的是从 DynamoDB 中删除的项目的值，该值应显示在查询窗格右侧的结果窗格中。如下所示：

  ```
  {
    "data": {
      "deletePost": {
        "id": "123",
        "author": "A new author",
        "title": "An empty story",
        "content": null,
        "url": "https://aws.amazon.com/appsync/",
        "ups": 6,
        "downs": 4,
        "version": 12
      }
    }
  }
  ```

只有调用的 `deletePost` 将项目从 DynamoDB 中实际删除，才会返回值。
+ 再次选择 **Execute query (执行查询)**。
+ 调用仍然成功，但没有返回任何值。

  ```
  {
    "data": {
      "deletePost": null
    }
  }
  ```

现在，让我们尝试删除一篇文章，但这次指定 `expectedValue`。但首先您需要创建一个新文章，因为您刚刚删除了一直在使用的文章。
+ 在 **Queries (查询)** 窗格中，粘贴以下变更：

  ```
  mutation addPost {
    addPost(
      id:123
      author: "AUTHORNAME"
      title: "Our second post!"
      content: "A new post."
      url: "https://aws.amazon.com/appsync/"
    ) {
      id
      author
      title
      content
      url
      ups
      downs
      version
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。
+ 新创建的文章的结果应出现在查询窗格右侧的结果窗格中。记下新建对象的 `id`，因为一会您将用到它。如下所示：

  ```
  {
    "data": {
      "addPost": {
        "id": "123",
        "author": "AUTHORNAME",
        "title": "Our second post!",
        "content": "A new post.",
        "url": "https://aws.amazon.com/appsync/",
        "ups": 1,
        "downs": 0,
        "version": 1
      }
    }
  }
  ```

现在让我们尝试删除这个文章，但放入 `expectedVersion` 的错误值：
+ 在 **Queries (查询)** 窗格中，粘贴以下更改。您还需要将 `id` 参数更新为您以前记下的值。

  ```
  mutation deletePost {
    deletePost(
      id:123
      expectedVersion: 9999
    ) {
      id
      author
      title
      content
      url
      ups
      downs
      version
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。

  ```
  {
    "data": {
      "deletePost": null
    },
    "errors": [
      {
        "path": [
          "deletePost"
        ],
        "data": {
          "id": "123",
          "author": "AUTHORNAME",
          "title": "Our second post!",
          "content": "A new post.",
          "url": "https://aws.amazon.com/appsync/",
          "ups": 1,
          "downs": 0,
          "version": 1
        },
        "errorType": "DynamoDB:ConditionalCheckFailedException",
        "locations": [
          {
            "line": 2,
            "column": 3
          }
        ],
        "message": "The conditional request failed (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException; Request ID: ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ)"
      }
    ]
  }
  ```

  请求失败，因为条件表达式的评估结果为 false：DynamoDB 中的文章的 `version` 值与参数中指定的 `expectedValue` 不匹配。对象的当前值返回到 GraphQL 响应的 `data` 部分的 `errors` 字段中。
+ 重试请求，但更正 `expectedVersion`：

  ```
  mutation deletePost {
    deletePost(
      id:123
      expectedVersion: 1
    ) {
      id
      author
      title
      content
      url
      ups
      downs
      version
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。
+ 这次请求成功，并返回从 DynamoDB 中删除的值：

  ```
  {
    "data": {
      "deletePost": {
        "id": "123",
        "author": "AUTHORNAME",
        "title": "Our second post!",
        "content": "A new post.",
        "url": "https://aws.amazon.com/appsync/",
        "ups": 1,
        "downs": 0,
        "version": 1
      }
    }
  }
  ```
+ 再次选择 **Execute query (执行查询)**。
+ 调用仍然成功，但这次没有返回任何值，因为已在 DynamoDB 中删除该文章。

```
{
  "data": {
    "deletePost": null
  }
}
```

## 设置 allPost 解析器 (DynamoDB Scan)


到目前为止，只有在您知道要查看的每篇文章的 `id` 时，才能使用该 API。让我们添加新的解析器，它可以返回表中的所有文章。
+ 选择**架构**选项卡。
+ 在 **Schema (架构)** 窗格中修改 `Query` 类型，添加新的 `allPost` 查询，如下所示：

  ```
  type Query {
      allPost(count: Int, nextToken: String): PaginatedPosts!
      getPost(id: ID): Post
  }
  ```
+ 添加新 `PaginationPosts` 类型：

  ```
  type PaginatedPosts {
      posts: [Post!]!
      nextToken: String
  }
  ```
+ 选择**保存**。
+ 在右侧的**数据类型**窗格中，找到 **Query** 类型上的新创建的 **allPost** 字段，然后选择**附加**。
+ 在**操作**菜单中，选择**更新运行时**，然后选择**单位解析器 (仅限 VTL)**。
+ 在 **Data source name (数据源名称)** 中，选择 **PostDynamoDBTable**。
+ 在 **Configure the request mapping template (配置请求映射模板)** 中，粘贴以下内容：

  ```
  {
      "version" : "2017-02-28",
      "operation" : "Scan"
      #if( ${context.arguments.count} )
          ,"limit": $util.toJson($context.arguments.count)
      #end
      #if( ${context.arguments.nextToken} )
          ,"nextToken": $util.toJson($context.arguments.nextToken)
      #end
  }
  ```

  此解析器有两个可选参数：`count` 指定单次调用可返回的项目数量上限；`nextToken` 可用于检索下一组结果（稍后您将展示 `nextToken` 的值来自何处）。
+ 在 **Configure the response mapping template (配置响应映射模板)** 中，粘贴以下内容：

  ```
  {
      "posts": $utils.toJson($context.result.items)
      #if( ${context.result.nextToken} )
          ,"nextToken": $util.toJson($context.result.nextToken)
      #end
  }
  ```

   **注意**：此响应映射模板与目前我们所用到的其他模板均不同。`allPost` 查询的结果是 `PaginatedPosts`，其中包含一组文章和一个分页标记。此对象的形状与 D Amazon AppSync ynamoDB 解析器返回的形状不同：帖子列表在 DynamoDB 解析器结果`items`中被调用，但会被调用。 Amazon AppSync `posts` `PaginatedPosts`
+ 选择**保存**。

有关 `Scan` 请求映射的更多信息，请参阅 [Scan](aws-appsync-resolver-mapping-template-reference-dynamodb-scan.md) 参考文档。

### 调用 API 以扫描所有文章


现在，解析器已经设置完毕， Amazon AppSync 知道如何将传入的`allPost`查询转换为 DynamoDB `Scan` 操作。现在您可以扫描整个表，检索所有文章。

在进行尝试之前，您需要在表中填充一些数据，因为您已经删除了之前使用的所有内容。
+ 选择 **Queries** 选项卡。
+ 在 **Queries (查询)** 窗格中，粘贴以下变更：

  ```
  mutation addPost {
    post1: addPost(id:1 author: "AUTHORNAME" title: "A series of posts, Volume 1" content: "Some content" url: "https://aws.amazon.com/appsync/" ) { title }
    post2: addPost(id:2 author: "AUTHORNAME" title: "A series of posts, Volume 2" content: "Some content" url: "https://aws.amazon.com/appsync/" ) { title }
    post3: addPost(id:3 author: "AUTHORNAME" title: "A series of posts, Volume 3" content: "Some content" url: "https://aws.amazon.com/appsync/" ) { title }
    post4: addPost(id:4 author: "AUTHORNAME" title: "A series of posts, Volume 4" content: "Some content" url: "https://aws.amazon.com/appsync/" ) { title }
    post5: addPost(id:5 author: "AUTHORNAME" title: "A series of posts, Volume 5" content: "Some content" url: "https://aws.amazon.com/appsync/" ) { title }
    post6: addPost(id:6 author: "AUTHORNAME" title: "A series of posts, Volume 6" content: "Some content" url: "https://aws.amazon.com/appsync/" ) { title }
    post7: addPost(id:7 author: "AUTHORNAME" title: "A series of posts, Volume 7" content: "Some content" url: "https://aws.amazon.com/appsync/" ) { title }
    post8: addPost(id:8 author: "AUTHORNAME" title: "A series of posts, Volume 8" content: "Some content" url: "https://aws.amazon.com/appsync/" ) { title }
    post9: addPost(id:9 author: "AUTHORNAME" title: "A series of posts, Volume 9" content: "Some content" url: "https://aws.amazon.com/appsync/" ) { title }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。

现在，让我们扫描表，每次返回 5 个结果。
+ 在**Queries (查询)** 窗格中粘贴以下查询：

  ```
  query allPost {
    allPost(count: 5) {
      posts {
        id
        title
      }
      nextToken
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。
+ 最前面的 5 个文章应出现在查询窗格右侧的结果窗格中。如下所示：

  ```
  {
    "data": {
      "allPost": {
        "posts": [
          {
            "id": "5",
            "title": "A series of posts, Volume 5"
          },
          {
            "id": "1",
            "title": "A series of posts, Volume 1"
          },
          {
            "id": "6",
            "title": "A series of posts, Volume 6"
          },
          {
            "id": "9",
            "title": "A series of posts, Volume 9"
          },
          {
            "id": "7",
            "title": "A series of posts, Volume 7"
          }
        ],
        "nextToken": "eyJ2ZXJzaW9uIjoxLCJ0b2tlbiI6IkFRSUNBSGo4eHR0RG0xWXhUa1F0cEhXMEp1R3B0M1B3eThOSmRvcG9ad2RHYjI3Z0lnRkJEdXdUK09hcnovRGhNTGxLTGdMUEFBQUI1akNDQWVJR0NTcUdTSWIzRFFFSEJxQ0NBZE13Z2dIUEFnRUFNSUlCeUFZSktvWklodmNOQVFjQk1CNEdDV0NHU0FGbEF3UUJMakFSQkF6ajFodkhKU1paT1pncTRaUUNBUkNBZ2dHWnJiR1dQWGxkMDB1N0xEdGY4Z2JsbktzRjRua1VCcks3TFJLcjZBTFRMeGFwVGJZMDRqOTdKVFQyYVRwSzdzbVdtNlhWWFVCTnFIOThZTzBWZHVkdDI2RlkxMHRqMDJ2QTlyNWJTUWpTbWh6NE5UclhUMG9KZWJSQ2JJbXBlaDRSVlg0Tis0WTVCN1IwNmJQWWQzOVhsbTlUTjBkZkFYMVErVCthaXZoNE5jMk50RitxVmU3SlJ5WmpzMEFkSGduM3FWd2VrOW5oeFVVd3JlK1loUks5QkRzemdiMDlmZmFPVXpzaFZ4cVJRbC93RURlOTcrRmVJdXZNby9NZ1F6dUdNbFRyalpNR3FuYzZBRnhwa0VlZTFtR0FwVDFISElUZlluakptYklmMGUzUmcxbVlnVHVSbDh4S0trNmR0QVoraEhLVDhuNUI3VnF4bHRtSnlNUXBrZGl6KzkyL3VzNDl4OWhrMnVxSW01ZFFwMjRLNnF0dm9ZK1BpdERuQTc5djhzb0grVytYT3VuQ2NVVDY4TVZ1Wk5KYkRuSEFSSEVlaTlVNVBTelU5RGZ6d2pPdmhqWDNJMWhwdWUrWi83MDVHVjlPQUxSTGlwZWZPeTFOZFhwZTdHRDZnQW00bUJUK2c1eC9Ec3ZDbWVnSDFDVXRTdHVuU1ZFa2JpZytQRC9oMUwyRTNqSHhVQldaa28yU256WUc0cG0vV1RSWkFVZHZuQT09In0="
      }
    }
  }
  ```

您可以看到 5 个结果，还有一个 `nextToken`，可用于获得下一组结果。
+ 更新 `allPost` 查询，加入上一组结果的 `nextToken`：

  ```
  query allPost {
    allPost(
      count: 5
      nextToken: "eyJ2ZXJzaW9uIjoxLCJ0b2tlbiI6IkFRSUNBSGo4eHR0RG0xWXhUa1F0cEhXMEp1R3B0M1B3eThOSmRvcG9ad2RHYjI3Z0lnRlluNktJRWl6V0ZlR3hJOVJkaStrZUFBQUI1akNDQWVJR0NTcUdTSWIzRFFFSEJxQ0NBZE13Z2dIUEFnRUFNSUlCeUFZSktvWklodmNOQVFjQk1CNEdDV0NHU0FGbEF3UUJMakFSQkF5cW8yUGFSZThnalFpemRCTUNBUkNBZ2dHWk1JODhUNzhIOFVUZGtpdFM2ZFluSWRyVDg4c2lkN1RjZzB2d1k3VGJTTWpSQ2U3WjY3TkUvU2I1dWNETUdDMmdmMHErSGJSL0pteGRzYzVEYnE1K3BmWEtBdU5jSENJdWNIUkJ0UHBPWVdWdCtsS2U5L1pNcWdocXhrem1RaXI1YnIvQkt6dU5hZmJCdE93NmtoM2Jna1BKM0RjWWhpMFBGbmhMVGg4TUVGSjBCcXg3RTlHR1V5N0tUS0JLZlV3RjFQZ0JRREdrNzFYQnFMK2R1S2IrVGtZZzVYMjFrc3NyQmFVTmNXZmhTeXE0ZUJHSWhqZWQ5c3VKWjBSSTc2ZnVQdlZkR3FLNENjQmxHYXhpekZnK2pKK1FneEU1SXduRTNYYU5TR0I4QUpmamR2bU1wbUk1SEdvWjlMUUswclczbG14RDRtMlBsaTNLaEVlcm9pem5zcmdINFpvcXIrN2ltRDN3QkJNd3BLbGQzNjV5Nnc4ZnMrK2FnbTFVOUlKOFFrOGd2bEgySHFROHZrZXBrMWlLdWRIQ25LaS9USnBlMk9JeEVPazVnRFlzRTRUU09HUlVJTkxYY2MvdW1WVEpBMUthV2hWTlAvdjNlSnlZQUszbWV6N2h5WHVXZ1BkTVBNWERQdTdjVnVRa3EwK3NhbGZOd2wvSUx4bHNyNDVwTEhuVFpyRWZvVlV1bXZ5S2VKY1RUU1lET05hM1NwWEd2UT09In0="
    ) {
      posts {
        id
        author
      }
      nextToken
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。
+ 其余的 4 个文章应出现在查询窗格右侧的结果窗格中。在这组结果中没有 `nextToken`，因为您已查看了所有 9 篇文章，没有其余文章了。如下所示：

  ```
  {
    "data": {
      "allPost": {
        "posts": [
          {
            "id": "2",
            "title": "A series of posts, Volume 2"
          },
          {
            "id": "3",
            "title": "A series of posts, Volume 3"
          },
          {
            "id": "4",
            "title": "A series of posts, Volume 4"
          },
          {
            "id": "8",
            "title": "A series of posts, Volume 8"
          }
        ],
        "nextToken": null
      }
    }
  }
  ```

## 设置 allPostsBy作者解析器（DynamoDB 查询）


除了扫描 DynamoDB 以查找所有文章以外，您还可以查询 DynamoDB 以检索特定作者创建的文章。您以前创建的 DynamoDB 表已具有一个名为 `author-index` 的 `GlobalSecondaryIndex`，您可以将其与 DynamoDB `Query` 操作一起使用以检索特定作者创建的所有文章。
+ 选择**架构**选项卡。
+ 在 **Schema (架构)** 窗格中修改 `Query` 类型，添加新的 `allPostsByAuthor` 查询，如下所示：

  ```
  type Query {
      allPostsByAuthor(author: String!, count: Int, nextToken: String): PaginatedPosts!
      allPost(count: Int, nextToken: String): PaginatedPosts!
      getPost(id: ID): Post
  }
  ```

   **注意**：这次使用与 `allPost` 查询相同的 `PaginatedPosts` 类型。
+ 选择**保存**。
+ 在右侧的 “**数据类型**” 窗格中，在 “**查询**” 类型上找到新创建的 “**allPostsBy作者**” 字段，然后选择 “**附加**”。
+ 在**操作**菜单中，选择**更新运行时**，然后选择**单位解析器 (仅限 VTL)**。
+ 在 **Data source name (数据源名称)** 中，选择 **PostDynamoDBTable**。
+ 在 **Configure the request mapping template (配置请求映射模板)** 中，粘贴以下内容：

  ```
  {
      "version" : "2017-02-28",
      "operation" : "Query",
      "index" : "author-index",
      "query" : {
        "expression": "author = :author",
          "expressionValues" : {
            ":author" : $util.dynamodb.toDynamoDBJson($context.arguments.author)
          }
      }
      #if( ${context.arguments.count} )
          ,"limit": $util.toJson($context.arguments.count)
      #end
      #if( ${context.arguments.nextToken} )
          ,"nextToken": "${context.arguments.nextToken}"
      #end
  }
  ```

  与 `allPost` 解析器相似，此解析器也有两个可选参数：`count`指定单次调用可返回的项目数量上限；`nextToken` 可用于检索下一组结果（`nextToken` 的值可从上次调用获得）。
+ 在 **Configure the response mapping template (配置响应映射模板)** 中，粘贴以下内容：

  ```
  {
      "posts": $utils.toJson($context.result.items)
      #if( ${context.result.nextToken} )
          ,"nextToken": $util.toJson($context.result.nextToken)
      #end
  }
  ```

   **注意**：此处使用的响应映射模板与 `allPost` 解析器中所用的相同。
+ 选择**保存**。

了解有关 `Query` 请求映射的更多信息，请参阅 [Query](aws-appsync-resolver-mapping-template-reference-dynamodb-query.md) 参考文档。

### 调用 API 以查询某一作者的所有文章


现在解析器已经设置好了， Amazon AppSync 知道如何将传入的`allPostsByAuthor`突变转换为针对索引的 DynamoDB 操作`Query`。`author-index`现在，您可以查询表，检索某一作者的所有文章。

但首先我们需要在表中再填充一些文章，因为目前所有文章都是同一作者。
+ 选择 **Queries** 选项卡。
+ 在 **Queries (查询)** 窗格中，粘贴以下变更：

  ```
  mutation addPost {
    post1: addPost(id:10 author: "Nadia" title: "The cutest dog in the world" content: "So cute. So very, very cute." url: "https://aws.amazon.com/appsync/" ) { author, title }
    post2: addPost(id:11 author: "Nadia" title: "Did you know...?" content: "AppSync works offline?" url: "https://aws.amazon.com/appsync/" ) { author, title }
    post3: addPost(id:12 author: "Steve" title: "I like GraphQL" content: "It's great" url: "https://aws.amazon.com/appsync/" ) { author, title }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。

现在，让我们查询表，返回作者为 `Nadia` 的所有文章。
+ 在**Queries (查询)** 窗格中粘贴以下查询：

  ```
  query allPostsByAuthor {
    allPostsByAuthor(author: "Nadia") {
      posts {
        id
        title
      }
      nextToken
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。
+ 作者为 `Nadia` 的全部文章应出现在查询窗格右侧的结果窗格中。如下所示：

  ```
  {
    "data": {
      "allPostsByAuthor": {
        "posts": [
          {
            "id": "10",
            "title": "The cutest dog in the world"
          },
          {
            "id": "11",
            "title": "Did you know...?"
          }
        ],
        "nextToken": null
      }
    }
  }
  ```

`Query` 的分页方式与 `Scan` 相同。例如，如果我们查找作者为 `AUTHORNAME` 的所有文章，每次显示 5 个结果。
+ 在**Queries (查询)** 窗格中粘贴以下查询：

  ```
  query allPostsByAuthor {
    allPostsByAuthor(
      author: "AUTHORNAME"
      count: 5
    ) {
      posts {
        id
        title
      }
      nextToken
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。
+ 作者为 `AUTHORNAME` 的全部文章应出现在查询窗格右侧的结果窗格中。如下所示：

  ```
  {
    "data": {
      "allPostsByAuthor": {
        "posts": [
          {
            "id": "6",
            "title": "A series of posts, Volume 6"
          },
          {
            "id": "4",
            "title": "A series of posts, Volume 4"
          },
          {
            "id": "2",
            "title": "A series of posts, Volume 2"
          },
          {
            "id": "7",
            "title": "A series of posts, Volume 7"
          },
          {
            "id": "1",
            "title": "A series of posts, Volume 1"
          }
        ],
        "nextToken": "eyJ2ZXJzaW9uIjoxLCJ0b2tlbiI6IkFRSUNBSGo4eHR0RG0xWXhUa1F0cEhXMEp1R3B0M1B3eThOSmRvcG9ad2RHYjI3Z0lnSExqRnVhVUR3ZUhEZ2QzNGJ2QlFuY0FBQUNqekNDQW9zR0NTcUdTSWIzRFFFSEJxQ0NBbnd3Z2dKNEFnRUFNSUlDY1FZSktvWklodmNOQVFjQk1CNEdDV0NHU0FGbEF3UUJMakFSQkF5Qkg4Yk1obW9LVEFTZHM3SUNBUkNBZ2dKQ3dISzZKNlJuN3pyYUVKY1pWNWxhSkNtZW1KZ0F5N1dhZkc2UEdTNHpNQzJycTkwZHFJTFV6Z25wck9Gd3pMS3VOQ2JvUXc3VDI5eCtnVExIbGg4S3BqbzB1YjZHQ3FwcDhvNDVmMG9JbDlmdS9JdjNXcFNNSXFKTXZ1MEVGVWs1VzJQaW5jZGlUaVRtZFdYWlU1bkV2NkgyRFBRQWZYYlNnSmlHSHFLbmJZTUZZM0FTdmRIL0hQaVZBb1RCMk1YZkg0eGJOVTdEbjZtRFNhb2QwbzdHZHJEWDNtODQ1UXBQUVNyUFhHemY0WDkyajhIdlBCSWE4Smcrb0RxbHozUVQ5N2FXUXdYWWU2S0h4emI1ejRITXdEdXEyRDRkYzhoMi9CbW10MzRMelVGUVIyaExSZGRaZ0xkdzF5cHJZdFZwY3dEc1d4UURBTzdOcjV2ZEp4VVR2TVhmODBRSnp1REhXREpTVlJLdDJwWmlpaXhXeGRwRmNod1BzQ3d2aVBqMGwrcWFFWU1jMXNQbENkVkFGem43VXJrSThWbS8wWHlwR2xZb3BSL2FkV0xVekgrbGMrYno1ZEM2SnVLVXdtY1EyRXlZeDZiS0Izbi9YdUViWGdFeU5PMWZTdE1rRlhyWmpvMVpzdlYyUFRjMzMrdEs0ZDhkNkZrdjh5VVR6WHhJRkxIaVNsOUx6VVdtT3BCaWhrTFBCT09jcXkyOHh1UmkzOEM3UFRqMmN6c3RkOUo1VUY0azBJdUdEbVZzM2xjdWg1SEJjYThIeXM2aEpvOG1HbFpMNWN6R2s5bi8vRE1EbDY3RlJraG5QNFNhSDBpZGI5VFEvMERLeFRBTUdhcWpPaEl5ekVqd2ZDQVJleFdlbldyOGlPVkhScDhGM25WZVdvbFRGK002N0xpdi9XNGJXdDk0VEg3b0laUU5lYmZYKzVOKy9Td25Hb1dyMTlWK0pEb2lIRVFLZ1cwMWVuYjZKUXo5Slh2Tm95ZzF3RnJPVmxGc2xwNlRHa1BlN2Rnd2IrWT0ifQ=="
      }
    }
  }
  ```
+ 用上次查询返回的值更新 `nextToken` 参数，如下所示：

  ```
  query allPostsByAuthor {
    allPostsByAuthor(
      author: "AUTHORNAME"
      count: 5
      nextToken: "eyJ2ZXJzaW9uIjoxLCJ0b2tlbiI6IkFRSUNBSGo4eHR0RG0xWXhUa1F0cEhXMEp1R3B0M1B3eThOSmRvcG9ad2RHYjI3Z0lnSExqRnVhVUR3ZUhEZ2QzNGJ2QlFuY0FBQUNqekNDQW9zR0NTcUdTSWIzRFFFSEJxQ0NBbnd3Z2dKNEFnRUFNSUlDY1FZSktvWklodmNOQVFjQk1CNEdDV0NHU0FGbEF3UUJMakFSQkF5Qkg4Yk1obW9LVEFTZHM3SUNBUkNBZ2dKQ3dISzZKNlJuN3pyYUVKY1pWNWxhSkNtZW1KZ0F5N1dhZkc2UEdTNHpNQzJycTkwZHFJTFV6Z25wck9Gd3pMS3VOQ2JvUXc3VDI5eCtnVExIbGg4S3BqbzB1YjZHQ3FwcDhvNDVmMG9JbDlmdS9JdjNXcFNNSXFKTXZ1MEVGVWs1VzJQaW5jZGlUaVRtZFdYWlU1bkV2NkgyRFBRQWZYYlNnSmlHSHFLbmJZTUZZM0FTdmRIL0hQaVZBb1RCMk1YZkg0eGJOVTdEbjZtRFNhb2QwbzdHZHJEWDNtODQ1UXBQUVNyUFhHemY0WDkyajhIdlBCSWE4Smcrb0RxbHozUVQ5N2FXUXdYWWU2S0h4emI1ejRITXdEdXEyRDRkYzhoMi9CbW10MzRMelVGUVIyaExSZGRaZ0xkdzF5cHJZdFZwY3dEc1d4UURBTzdOcjV2ZEp4VVR2TVhmODBRSnp1REhXREpTVlJLdDJwWmlpaXhXeGRwRmNod1BzQ3d2aVBqMGwrcWFFWU1jMXNQbENkVkFGem43VXJrSThWbS8wWHlwR2xZb3BSL2FkV0xVekgrbGMrYno1ZEM2SnVLVXdtY1EyRXlZeDZiS0Izbi9YdUViWGdFeU5PMWZTdE1rRlhyWmpvMVpzdlYyUFRjMzMrdEs0ZDhkNkZrdjh5VVR6WHhJRkxIaVNsOUx6VVdtT3BCaWhrTFBCT09jcXkyOHh1UmkzOEM3UFRqMmN6c3RkOUo1VUY0azBJdUdEbVZzM2xjdWg1SEJjYThIeXM2aEpvOG1HbFpMNWN6R2s5bi8vRE1EbDY3RlJraG5QNFNhSDBpZGI5VFEvMERLeFRBTUdhcWpPaEl5ekVqd2ZDQVJleFdlbldyOGlPVkhScDhGM25WZVdvbFRGK002N0xpdi9XNGJXdDk0VEg3b0laUU5lYmZYKzVOKy9Td25Hb1dyMTlWK0pEb2lIRVFLZ1cwMWVuYjZKUXo5Slh2Tm95ZzF3RnJPVmxGc2xwNlRHa1BlN2Rnd2IrWT0ifQ=="
    ) {
      posts {
        id
        title
      }
      nextToken
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。
+ 作者为 `AUTHORNAME` 的剩余文章应出现在查询窗格右侧的结果窗格中。如下所示：

  ```
  {
    "data": {
      "allPostsByAuthor": {
        "posts": [
          {
            "id": "8",
            "title": "A series of posts, Volume 8"
          },
          {
            "id": "5",
            "title": "A series of posts, Volume 5"
          },
          {
            "id": "3",
            "title": "A series of posts, Volume 3"
          },
          {
            "id": "9",
            "title": "A series of posts, Volume 9"
          }
        ],
        "nextToken": null
      }
    }
  }
  ```

## 使用集


到目前为止，该`Post`类型一直是一个平面 key/value 对象。您还可以使用 Amazon AppSyncDynamo数据库解析器对复杂的对象进行建模，例如集合、列表和地图。

让我们更新 `Post` 类型，加入标签。一篇文章可以具有 0 个或更多标签，这些标签作为字符串集存储在 DynamoDB 中。您还将设置一些变更，用于添加并删除标签；还要用一个新查询扫描具有特定标签的文章。
+ 选择**架构**选项卡。
+ 在 **Schema (架构)** 窗格中修改 `Post` 类型，添加新的 `tags` 字段，如下所示：

  ```
  type Post {
    id: ID!
    author: String
    title: String
    content: String
    url: String
    ups: Int!
    downs: Int!
    version: Int!
    tags: [String!]
  }
  ```
+ 在 **Schema (架构)** 窗格中修改 `Query` 类型，添加新的 `allPostsByTag` 查询，如下所示：

  ```
  type Query {
    allPostsByTag(tag: String!, count: Int, nextToken: String): PaginatedPosts!
    allPostsByAuthor(author: String!, count: Int, nextToken: String): PaginatedPosts!
    allPost(count: Int, nextToken: String): PaginatedPosts!
    getPost(id: ID): Post
  }
  ```
+ 在 **Schema (架构)** 窗格中修改 `Mutation` 类型，添加新的 `addTag` 和 `removeTag` 变更，如下所示：

  ```
  type Mutation {
    addTag(id: ID!, tag: String!): Post
    removeTag(id: ID!, tag: String!): Post
    deletePost(id: ID!, expectedVersion: Int): Post
    upvotePost(id: ID!): Post
    downvotePost(id: ID!): Post
    updatePost(
      id: ID!,
      author: String,
      title: String,
      content: String,
      url: String,
      expectedVersion: Int!
    ): Post
    addPost(
      author: String!,
      title: String!,
      content: String!,
      url: String!
    ): Post!
  }
  ```
+ 选择**保存**。
+ 在右侧**的数据类型**窗格中，在**查询**类型上找到新创建的**allPostsBy标签**字段，然后选择**附加**。
+ 在 **Data source name (数据源名称)** 中，选择 **PostDynamoDBTable**。
+ 在 **Configure the request mapping template (配置请求映射模板)** 中，粘贴以下内容：

  ```
  {
      "version" : "2017-02-28",
      "operation" : "Scan",
      "filter": {
        "expression": "contains (tags, :tag)",
          "expressionValues": {
            ":tag": $util.dynamodb.toDynamoDBJson($context.arguments.tag)
          }
      }
      #if( ${context.arguments.count} )
          ,"limit": $util.toJson($context.arguments.count)
      #end
      #if( ${context.arguments.nextToken} )
          ,"nextToken": $util.toJson($context.arguments.nextToken)
      #end
  }
  ```
+ 在 **Configure the response mapping template (配置响应映射模板)** 中，粘贴以下内容：

  ```
  {
      "posts": $utils.toJson($context.result.items)
      #if( ${context.result.nextToken} )
          ,"nextToken": $util.toJson($context.result.nextToken)
      #end
  }
  ```
+ 选择**保存**。
+ 在右侧的**数据类型**窗格中，找到 **Mutation** 类型上的新创建的 **addTag** 字段，然后选择**附加**。
+ 在 **Data source name (数据源名称)** 中，选择 **PostDynamoDBTable**。
+ 在 **Configure the request mapping template (配置请求映射模板)** 中，粘贴以下内容：

  ```
  {
      "version" : "2017-02-28",
      "operation" : "UpdateItem",
      "key" : {
          "id" : $util.dynamodb.toDynamoDBJson($context.arguments.id)
      },
      "update" : {
          "expression" : "ADD tags :tags, version :plusOne",
          "expressionValues" : {
              ":tags" : { "SS": [ $util.toJson($context.arguments.tag) ] },
              ":plusOne" : { "N" : 1 }
          }
      }
  }
  ```
+ 在 **Configure the response mapping template (配置响应映射模板)** 中，粘贴以下内容：

  ```
  $utils.toJson($context.result)
  ```
+ 选择**保存**。
+ 在右侧的**数据类型**窗格中，找到 **Mutation** 类型上的新创建的 **removeTag** 字段，然后选择**附加**。
+ 在 **Data source name (数据源名称)** 中，选择 **PostDynamoDBTable**。
+ 在 **Configure the request mapping template (配置请求映射模板)** 中，粘贴以下内容：

  ```
  {
      "version" : "2017-02-28",
      "operation" : "UpdateItem",
      "key" : {
          "id" : $util.dynamodb.toDynamoDBJson($context.arguments.id)
      },
      "update" : {
          "expression" : "DELETE tags :tags ADD version :plusOne",
          "expressionValues" : {
              ":tags" : { "SS": [ $util.toJson($context.arguments.tag) ] },
              ":plusOne" : { "N" : 1 }
          }
      }
  }
  ```
+ 在 **Configure the response mapping template (配置响应映射模板)** 中，粘贴以下内容：

  ```
  $utils.toJson($context.result)
  ```
+ 选择**保存**。

### 调用 API 以处理标签


现在您已经设置了解析器， Amazon AppSync 知道如何将传入的`addTag``removeTag`、和`allPostsByTag`请求转换为 D `UpdateItem` ynamo `Scan` DB 和操作。

我们选择您之前创建的一个文章进行尝试。例如，我们使用作者为 `Nadia` 的一篇文章。
+ 选择 **Queries** 选项卡。
+ 在**Queries (查询)** 窗格中粘贴以下查询：

  ```
  query allPostsByAuthor {
    allPostsByAuthor(
      author: "Nadia"
    ) {
      posts {
        id
        title
      }
      nextToken
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。
+ Nadia 的全部文章应出现在查询窗格右侧的结果窗格中。如下所示：

  ```
  {
    "data": {
      "allPostsByAuthor": {
        "posts": [
          {
            "id": "10",
            "title": "The cutest dog in the world"
          },
          {
            "id": "11",
            "title": "Did you known...?"
          }
        ],
        "nextToken": null
      }
    }
  }
  ```
+ 让我们使用标题为 `"The cutest dog in the world"` 的文章。记下其 `id`，因为您稍后将用到它。

现在，让我们尝试添加一个 `dog` 标签。
+ 在 **Queries (查询)** 窗格中，粘贴以下更改。您还需要将 `id` 参数更新为您以前记下的值。

  ```
  mutation addTag {
    addTag(id:10 tag: "dog") {
      id
      title
      tags
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。
+ 将使用新标签更新该文章。

  ```
  {
    "data": {
      "addTag": {
        "id": "10",
        "title": "The cutest dog in the world",
        "tags": [
          "dog"
        ]
      }
    }
  }
  ```

您可以添加更多标签，如下所示：
+ 更新变更以将 `tag` 参数更改为 `puppy`。

  ```
  mutation addTag {
    addTag(id:10 tag: "puppy") {
      id
      title
      tags
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。
+ 将使用新标签更新该文章。

  ```
  {
    "data": {
      "addTag": {
        "id": "10",
        "title": "The cutest dog in the world",
        "tags": [
          "dog",
          "puppy"
        ]
      }
    }
  }
  ```

您也可以删除标签：
+ 在 **Queries (查询)** 窗格中，粘贴以下更改。您还需要将 `id` 参数更新为您以前记下的值。

  ```
  mutation removeTag {
    removeTag(id:10 tag: "puppy") {
      id
      title
      tags
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。
+ 文章已更新，`puppy` 标签已删除。

  ```
  {
    "data": {
      "addTag": {
        "id": "10",
        "title": "The cutest dog in the world",
        "tags": [
          "dog"
        ]
      }
    }
  }
  ```

您也可以搜索所有具有标签的文章：
+ 在**Queries (查询)** 窗格中粘贴以下查询：

  ```
  query allPostsByTag {
    allPostsByTag(tag: "dog") {
      posts {
        id
        title
        tags
      }
      nextToken
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。
+ 将返回具有 `dog` 标签的所有文章，如下所示：

  ```
  {
    "data": {
      "allPostsByTag": {
        "posts": [
          {
            "id": "10",
            "title": "The cutest dog in the world",
            "tags": [
              "dog",
              "puppy"
            ]
          }
        ],
        "nextToken": null
      }
    }
  }
  ```

## 使用列表和映射


除了使用 DynamoDB 集以外，您还可以使用 DynamoDB 列表和映射对单个对象中的复杂数据进行建模。

我们可以为文章添加评论功能。这会建模为 DynamoDB 中的 `Post` 对象上的映射对象列表。

 **注意**：在真正的应用程序中，您会在评论自身的表中对评论进行建模。在本教程中，您仅将评论添加到 `Post` 表。
+ 选择**架构**选项卡。
+ 在 **Schema (架构)** 窗格中，添加新的 `Comment` 类型，如下所示：

  ```
  type Comment {
      author: String!
      comment: String!
  }
  ```
+ 在 **Schema (架构)** 窗格中修改 `Post` 类型，添加新的 `comments` 字段，如下所示：

  ```
  type Post {
    id: ID!
    author: String
    title: String
    content: String
    url: String
    ups: Int!
    downs: Int!
    version: Int!
    tags: [String!]
    comments: [Comment!]
  }
  ```
+ 在 **Schema (架构)** 窗格中修改 `Mutation` 类型，添加新的 `addComment` 变更，如下所示：

  ```
  type Mutation {
    addComment(id: ID!, author: String!, comment: String!): Post
    addTag(id: ID!, tag: String!): Post
    removeTag(id: ID!, tag: String!): Post
    deletePost(id: ID!, expectedVersion: Int): Post
    upvotePost(id: ID!): Post
    downvotePost(id: ID!): Post
    updatePost(
      id: ID!,
      author: String,
      title: String,
      content: String,
      url: String,
      expectedVersion: Int!
    ): Post
    addPost(
      author: String!,
      title: String!,
      content: String!,
      url: String!
    ): Post!
  }
  ```
+ 选择**保存**。
+ 在右侧的**数据类型**窗格中，找到 **Mutation** 类型上的新创建的 **addComment** 字段，然后选择**附加**。
+ 在 **Data source name (数据源名称)** 中，选择 **PostDynamoDBTable**。
+ 在 **Configure the request mapping template (配置请求映射模板)** 中，粘贴以下内容：

  ```
  {
    "version" : "2017-02-28",
    "operation" : "UpdateItem",
    "key" : {
      "id" : $util.dynamodb.toDynamoDBJson($context.arguments.id)
    },
    "update" : {
      "expression" : "SET comments = list_append(if_not_exists(comments, :emptyList), :newComment) ADD version :plusOne",
      "expressionValues" : {
        ":emptyList": { "L" : [] },
        ":newComment" : { "L" : [
          { "M": {
            "author": $util.dynamodb.toDynamoDBJson($context.arguments.author),
            "comment": $util.dynamodb.toDynamoDBJson($context.arguments.comment)
            }
          }
        ] },
        ":plusOne" : $util.dynamodb.toDynamoDBJson(1)
      }
    }
  }
  ```

  此更新表达式将一个列表（包含新评论）追加到现有的 `comments` 列表中。如果这个列表不存在，将创建它。
+ 在 **Configure the response mapping template (配置响应映射模板)** 中，粘贴以下内容：

  ```
  $utils.toJson($context.result)
  ```
+ 选择**保存**。

### 调用 API 以添加评论


现在您已经设置了解析器， Amazon AppSync 知道如何将传入的`addComment`请求转换为 DynamoDB 操作`UpdateItem`。

让我们尝试在您已添加标签的文章中添加评论。
+ 选择 **Queries** 选项卡。
+ 在**Queries (查询)** 窗格中粘贴以下查询：

  ```
  mutation addComment {
    addComment(
      id:10
      author: "Steve"
      comment: "Such a cute dog."
    ) {
      id
      comments {
        author
        comment
      }
    }
  }
  ```
+ 选择 **Execute query (执行查询)**（橙色播放按钮）。
+ Nadia 的全部文章应出现在查询窗格右侧的结果窗格中。如下所示：

  ```
  {
    "data": {
      "addComment": {
        "id": "10",
        "comments": [
          {
            "author": "Steve",
            "comment": "Such a cute dog."
          }
        ]
      }
    }
  }
  ```

如果您多次执行该请求，列表中将追加多条评论。

## 结论


在本教程中，您构建了一个 API，允许我们使用 Amazon AppSync 和 GraphQL 在 DynamoDB 中操作 Post 对象。有关更多信息，请参阅[解析器映射模板参考](resolver-mapping-template-reference.md#aws-appsync-resolver-mapping-template-reference)。

要进行清理，您可以从控制台中删除 AppSync GraphQL API。

要删除 DynamoDB 表和您为本教程创建的 IAM 角色，您可以运行以下命令来删除`AWSAppSyncTutorialForAmazonDynamoDB`堆栈，或者访问 Amazon CloudFormation 控制台删除堆栈：

```
aws cloudformation delete-stack \
    --stack-name AWSAppSyncTutorialForAmazonDynamoDB
```

# 在中使用 Amazon Lambda 解析器 Amazon AppSync
使用 Amazon Lambda 解析器

**注意**  
我们现在主要支持 APPSYNC\$1JS 运行时系统及其文档。请考虑使用 APPSYNC\$1JS 运行时系统和[此处](https://docs.amazonaws.cn/appsync/latest/devguide/tutorials-js.html)的指南。

你可以 Amazon Lambda 和一起使用 Amazon AppSync 来解析任何 GraphQL 字段。例如，GraphQL 查询可能会向 Amazon Relational Database Service (Amazon RDS) 实例发送调用，而 GraphQL 变更可能会写入到 Amazon Kinesis 流。在本节中，我们说明了如何编写 Lambda 函数，以根据 GraphQL 字段操作调用执行业务逻辑。

## 创建 Lambda 函数


以下示例显示了一个使用 `Node.js` 编写的 Lambda 函数，该函数对博客文章应用程序包含的博客文章执行各种操作。

```
exports.handler = (event, context, callback) => {
    console.log("Received event {}", JSON.stringify(event, 3));
    var posts = {
         "1": {"id": "1", "title": "First book", "author": "Author1", "url": "https://amazon.com/", "content": "SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1", "ups": "100", "downs": "10"},
         "2": {"id": "2", "title": "Second book", "author": "Author2", "url": "https://amazon.com", "content": "SAMPLE TEXT AUTHOR 2 SAMPLE TEXT AUTHOR 2 SAMPLE TEXT", "ups": "100", "downs": "10"},
         "3": {"id": "3", "title": "Third book", "author": "Author3", "url": null, "content": null, "ups": null, "downs": null },
         "4": {"id": "4", "title": "Fourth book", "author": "Author4", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4", "ups": "1000", "downs": "0"},
         "5": {"id": "5", "title": "Fifth book", "author": "Author5", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT", "ups": "50", "downs": "0"} };

    var relatedPosts = {
        "1": [posts['4']],
        "2": [posts['3'], posts['5']],
        "3": [posts['2'], posts['1']],
        "4": [posts['2'], posts['1']],
        "5": []
    };

    console.log("Got an Invoke Request.");
    switch(event.field) {
        case "getPost":
            var id = event.arguments.id;
            callback(null, posts[id]);
            break;
        case "allPosts":
            var values = [];
            for(var d in posts){
                values.push(posts[d]);
            }
            callback(null, values);
            break;
        case "addPost":
            // return the arguments back
            callback(null, event.arguments);
            break;
        case "addPostErrorWithData":
            var id = event.arguments.id;
            var result = posts[id];
            // attached additional error information to the post
            result.errorMessage = 'Error with the mutation, data has changed';
            result.errorType = 'MUTATION_ERROR';
            callback(null, result);
            break;
        case "relatedPosts":
            var id = event.source.id;
            callback(null, relatedPosts[id]);
            break;
        default:
            callback("Unknown field, unable to resolve" + event.field, null);
            break;
    }
};
```

该 Lambda 函数按 ID 检索文章，添加文章，检索文章列表以及获取给定文章的相关文章。

 **注意：**Lambda 函数对 `event.field` 执行 `switch` 语句以确定当前解析的字段。

使用 Amazon 管理控制台或堆栈创建此 Lambda 函数。 Amazon CloudFormation 要从 CloudFormation 堆栈创建函数，可以使用以下 Amazon Command Line Interface (Amazon CLI) 命令：

```
aws cloudformation create-stack --stack-name AppSyncLambdaExample \
--template-url https://s3.us-west-2.amazonaws.com/awsappsync/resources/lambda/LambdaCFTemplate.yaml \
--capabilities CAPABILITY_NAMED_IAM
```

您也可以从以下位置在自己的 Amazon 账户中启动美国西部（俄勒冈） Amazon 地区的 Amazon CloudFormation 堆栈：

[https://console.amazonaws.cn/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3.us-west-2.amazonaws.com/awsappsync/resources/lambda/LambdaCFTemplate.yaml](https://console.amazonaws.cn/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3.us-west-2.amazonaws.com/awsappsync/resources/lambda/LambdaCFTemplate.yaml)

## 为 Lambda 配置数据来源


在创建 Lambda 函数后，在 Amazon AppSync 控制台中导航到您的 GraphQL API，然后选择**数据来源**选项卡。

选择**创建数据来源**，输入友好的**数据来源名称**（例如 **Lambda**），然后为**数据来源类型**选择 **Amazon Lambda 函数**。对于**区域**，选择与您的函数相同的区域。（如果您从提供的 CloudFormation 堆栈中创建了函数，则该函数可能在 **US-WEST-2** 中。） 对于**函数 ARN**，选择您的 Lambda 函数的 Amazon 资源名称 (ARN)。

选择 Lambda 函数后，您可以创建一个新 Amazon Identity and Access Management (IAM) 角色（为其 Amazon AppSync 分配相应的权限），也可以选择具有以下内联策略的现有角色：

------
#### [ JSON ]

****  

```
{
    "Version":"2012-10-17",		 	 	 
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "lambda:InvokeFunction"
            ],
            "Resource": "arn:aws:lambda:us-east-1:111122223333:function:LAMBDA_FUNCTION"
        }
    ]
}
```

------

您还必须为 IAM 角色建立信任关系 Amazon AppSync ，如下所示：

------
#### [ JSON ]

****  

```
{
    "Version":"2012-10-17",		 	 	 
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "appsync.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
```

------

## 创建 GraphQL 架构


数据来源现已连接到您的 Lambda 函数，请创建 GraphQL 架构。

在 Amazon AppSync 控制台的架构编辑器中，确保您的架构与以下架构相匹配：

```
schema {
    query: Query
    mutation: Mutation
}

type Query {
    getPost(id:ID!): Post
    allPosts: [Post]
}

type Mutation {
    addPost(id: ID!, author: String!, title: String, content: String, url: String): Post!
}

type Post {
    id: ID!
    author: String!
    title: String
    content: String
    url: String
    ups: Int
    downs: Int
    relatedPosts: [Post]
}
```

## 配置解析器


您现已注册了 Lambda 数据来源和有效的 GraphQL 架构，您可以使用解析器将 GraphQL 字段连接到 Lambda 数据来源。

要创建解析器，您需要使用映射模板。要了解映射模板的更多信息，请参阅[Resolver Mapping Template Overview](resolver-mapping-template-reference-overview.md#aws-appsync-resolver-mapping-template-reference-overview)。

有关 Lambda 映射模板的更多信息，请参阅[Resolver mapping template reference for Lambda](resolver-mapping-template-reference-lambda.md#aws-appsync-resolver-mapping-template-reference-lambda)。

在该步骤中，您将一个解析器附加到以下字段的 Lambda 函数：`getPost(id:ID!): Post`、`allPosts: [Post]`、`addPost(id: ID!, author: String!, title: String, content: String, url: String): Post!` 和 `Post.relatedPosts: [Post]`。

在 Amazon AppSync 控制台的架构编辑器中，在右侧选择**连接解析器**。`getPost(id:ID!): Post`

然后，在**操作**菜单中，选择**更新运行时**，然后选择**单位解析器 (仅限 VTL)**。

然后，选择您的 Lambda 数据来源。在**请求映射模板**部分中，选择 **Invoke And Forward Arguments (调用并转发参数)**。

修改 `payload` 对象，添加字段名称。您的模板应该类似以下内容：

```
{
    "version": "2017-02-28",
    "operation": "Invoke",
    "payload": {
        "field": "getPost",
        "arguments":  $utils.toJson($context.arguments)
    }
}
```

在**响应映射模板**部分中，选择 **Return Lambda Result (返回 Lambda 结果)**。

在本例中，按原样使用基本模板。它应该类似以下内容：

```
$utils.toJson($context.result)
```

选择**保存**。您已成功附加了您的首个解析器。针对其余字段重复此操作，如下所示：

对于 `addPost(id: ID!, author: String!, title: String, content: String, url: String): Post!` 请求映射模板：

```
{
    "version": "2017-02-28",
    "operation": "Invoke",
    "payload": {
        "field": "addPost",
        "arguments":  $utils.toJson($context.arguments)
    }
}
```

对于 `addPost(id: ID!, author: String!, title: String, content: String, url: String): Post!` 响应映射模板：

```
$utils.toJson($context.result)
```

对于 `allPosts: [Post]` 请求映射模板：

```
{
    "version": "2017-02-28",
    "operation": "Invoke",
    "payload": {
        "field": "allPosts"
    }
}
```

对于 `allPosts: [Post]` 响应映射模板：

```
$utils.toJson($context.result)
```

对于 `Post.relatedPosts: [Post]` 请求映射模板：

```
{
    "version": "2017-02-28",
    "operation": "Invoke",
    "payload": {
        "field": "relatedPosts",
        "source":  $utils.toJson($context.source)
    }
}
```

对于 `Post.relatedPosts: [Post]` 响应映射模板：

```
$utils.toJson($context.result)
```

## 测试您的 GraphQL API


现在您的 Lambda 函数已与 GraphQL 解析器连接，您可以使用控制台或客户端应用程序运行一些变更和查询。

在 Amazon AppSync 控制台的左侧，选择 **Quer** ies，然后粘贴以下代码：

### addPost 变更


```
mutation addPost {
    addPost(
        id: 6
        author: "Author6"
        title: "Sixth book"
        url: "https://www.amazon.com/"
        content: "This is the book is a tutorial for using GraphQL with Amazon AppSync."
    ) {
        id
        author
        title
        content
        url
        ups
        downs
    }
}
```

### getPost 查询


```
query getPost {
    getPost(id: "2") {
        id
        author
        title
        content
        url
        ups
        downs
    }
}
```

### allPosts 查询


```
query allPosts {
    allPosts {
        id
        author
        title
        content
        url
        ups
        downs
        relatedPosts {
            id
            title
        }
    }
}
```

## 返回错误


解析任何给定的字段可能会导致错误。使用 Amazon AppSync，您可以从以下来源引发错误：
+ 请求或响应映射模板
+ Lambda 函数

### 从映射模板


要故意引发错误，您可以使用 Velocity 模板语言 (VTL) 模板中的 `$utils.error` 帮助程序方法。它可接收 `errorMessage`、`errorType` 以及可选的 `data` 值作为参数。出现错误后，`data` 对于将额外的数据返回客户端很有用。在 GraphQL 最终响应中，`data` 对象将添加到 `errors`。

以下示例显示了如何在 `Post.relatedPosts: [Post]` 响应映射模板中使用它：

```
$utils.error("Failed to fetch relatedPosts", "LambdaFailure", $context.result)
```

这将生成与以下内容类似的 GraphQL 响应：

```
{
    "data": {
        "allPosts": [
            {
                "id": "2",
                "title": "Second book",
                "relatedPosts": null
            },
            ...
        ]
    },
    "errors": [
        {
            "path": [
                "allPosts",
                0,
                "relatedPosts"
            ],
            "errorType": "LambdaFailure",
            "locations": [
                {
                    "line": 5,
                    "column": 5
                }
            ],
            "message": "Failed to fetch relatedPosts",
            "data": [
                {
                  "id": "2",
                  "title": "Second book"
                },
                {
                  "id": "1",
                  "title": "First book"
                }
            ]
        }
    ]
}
```

由于出现错误，导致 `allPosts[0].relatedPosts` 为 *null*，而 `data.errors[0]` 对象中存在 `errorMessage`、`errorType` 和 `data`。

### 从 Lambda 函数


Amazon AppSync 还可以理解 Lambda 函数引发的错误。Lambda 编程模型允许您引发*处理的* 错误。如果 Lambda 函数抛出错误， Amazon AppSync 则无法解析当前字段。仅在响应中设置从 Lambda 返回的错误消息。目前，您无法通过从 Lambda 函数中引发错误，将任何无关数据传回到客户端。

 **注意**：如果您的 Lambda 函数引发了*未处理*的错误，则 Amazon AppSync 使用 Lambda 设置的错误消息。

以下 Lambda 函数会引发错误：

```
exports.handler = (event, context, callback) => {
    console.log("Received event {}", JSON.stringify(event, 3));
    callback("I fail. Always.");
};
```

这将返回与以下内容类似的 GraphQL 响应：

```
{
    "data": {
        "allPosts": [
            {
                "id": "2",
                "title": "Second book",
                "relatedPosts": null
            },
            ...
        ]
    },
    "errors": [
        {
            "path": [
                "allPosts",
                0,
                "relatedPosts"
            ],
            "errorType": "Lambda:Handled",
            "locations": [
                {
                    "line": 5,
                    "column": 5
                }
            ],
            "message": "I fail. Always."
        }
    ]
}
```

## 高级使用案例：批处理


该示例中的 Lambda 函数具有一个 `relatedPosts` 字段，它返回给定文章的相关文章列表。在示例查询中，从 Lambda 函数中调用 `allPosts` 字段将返回 5 篇文章。由于我们指定还希望为每个返回的文章解析 `relatedPosts`，因此，将 `relatedPosts` 字段操作调用 5 次。

```
query allPosts {
    allPosts {   // 1 Lambda invocation - yields 5 Posts
        id
        author
        title
        content
        url
        ups
        downs
        relatedPosts {   // 5 Lambda invocations - each yields 5 posts
            id
            title
        }
    }
}
```

虽然这在该特定示例中听起来可能并不严重，但这种累积的过度获取可能会迅速降低应用程序的性能。

如果您要针对同一查询中返回的相关 `Posts` 再次提取 `relatedPosts`，调用数量将显著增加。

```
query allPosts {
    allPosts {   // 1 Lambda invocation - yields 5 Posts
        id
        author
        title
        content
        url
        ups
        downs
        relatedPosts {   // 5 Lambda invocations - each yield 5 posts = 5 x 5 Posts
            id
            title
            relatedPosts {  // 5 x 5 Lambda invocations - each yield 5 posts = 25 x 5 Posts
                id
                title
                author
            }
        }
    }
}
```

在这个相对简单的查询中， Amazon AppSync 将调用 Lambda 函数 1 \$1 5 \$1 25 = 31 次。

这是相当常见的挑战，常被称为 N\$11 问题（在本例中 N = 5），会导致延迟增加，以及应用程序费用提升。

我们解决此问题的方式是批处理类似的字段解析器请求。在该示例中，Lambda 函数可能会解析给定批次的文章的相关文章列表，而不是让 Lambda 函数解析单个给定文章的相关文章列表。

为了演示此操作，让我们将 `Post.relatedPosts: [Post]` 解析器转换为启用批处理的解析器。

在 Amazon AppSync 控制台的右侧，选择现有的解`Post.relatedPosts: [Post]`析器。将请求映射模板改为以下内容：

```
{
    "version": "2017-02-28",
    "operation": "BatchInvoke",
    "payload": {
        "field": "relatedPosts",
        "source":  $utils.toJson($context.source)
    }
}
```

只有 `operation` 字段由 `Invoke` 改为了 `BatchInvoke`。负载字段现在成为模板中指定的任何内容的数组。在该示例中，Lambda 函数收到以下内容以作为输入：

```
[
    {
        "field": "relatedPosts",
        "source": {
            "id": 1
        }
    },
    {
        "field": "relatedPosts",
        "source": {
            "id": 2
        }
    },
    ...
]
```

如果在请求映射模板中指定了 `BatchInvoke`，Lambda 函数将收到请求列表并返回结果列表。

具体而言，结果列表必须与请求有效载荷条目的大小和顺序相匹配，这样 Amazon AppSync 才能相应地匹配结果。

在该批处理示例中，Lambda 函数返回一批结果，如下所示：

```
[
    [{"id":"2","title":"Second book"}, {"id":"3","title":"Third book"}],   // relatedPosts for id=1
    [{"id":"3","title":"Third book"}]                                                             // relatedPosts for id=2
]
```

使用 Node.js 编写的以下 Lambda 函数说明了 `Post.relatedPosts` 字段的批处理功能，如下所示：

```
exports.handler = (event, context, callback) => {
    console.log("Received event {}", JSON.stringify(event, 3));
    var posts = {
         "1": {"id": "1", "title": "First book", "author": "Author1", "url": "https://amazon.com/", "content": "SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1", "ups": "100", "downs": "10"},
         "2": {"id": "2", "title": "Second book", "author": "Author2", "url": "https://amazon.com", "content": "SAMPLE TEXT AUTHOR 2 SAMPLE TEXT AUTHOR 2 SAMPLE TEXT", "ups": "100", "downs": "10"},
         "3": {"id": "3", "title": "Third book", "author": "Author3", "url": null, "content": null, "ups": null, "downs": null },
         "4": {"id": "4", "title": "Fourth book", "author": "Author4", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4", "ups": "1000", "downs": "0"},
         "5": {"id": "5", "title": "Fifth book", "author": "Author5", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT", "ups": "50", "downs": "0"} };

    var relatedPosts = {
        "1": [posts['4']],
        "2": [posts['3'], posts['5']],
        "3": [posts['2'], posts['1']],
        "4": [posts['2'], posts['1']],
        "5": []
    };

    console.log("Got a BatchInvoke Request. The payload has %d items to resolve.", event.length);
    // event is now an array
    var field = event[0].field;
    switch(field) {
        case "relatedPosts":
            var results = [];
            // the response MUST contain the same number
            // of entries as the payload array
            for (var i=0; i< event.length; i++) {
                console.log("post {}", JSON.stringify(event[i].source));
                results.push(relatedPosts[event[i].source.id]);
            }
            console.log("results {}", JSON.stringify(results));
            callback(null, results);
            break;
        default:
            callback("Unknown field, unable to resolve" + field, null);
            break;
    }
};
```

### 返回单个错误


以前的示例表明，可以从 Lambda 函数中返回单个错误，或者从映射模板中引发错误。对于批处理调用，从 Lambda 函数中引发错误会将整个批次标记为失败。对于发生不可恢复错误的特定场景（例如，到数据存储的连接失败），这可能是可以接受的。不过，如果批次中的某些项目成功，而其他项目失败，则可能会同时返回错误和有效的数据。由于 Amazon AppSync 需要批量响应才能列出与批次原始大小相匹配的元素，因此您必须定义一种能够区分有效数据和错误的数据结构。

例如，如果 Lambda 函数预计返回一批相关文章，您可以选择返回 `Response` 对象列表，其中每个对象具有可选的 *data*、*errorMessage* 和 *errorType* 字段。如果出现 *errorMessage* 字段，则表示出现错误。

以下代码说明了如何更新 Lambda 函数：

```
exports.handler = (event, context, callback) => {
    console.log("Received event {}", JSON.stringify(event, 3));
    var posts = {
         "1": {"id": "1", "title": "First book", "author": "Author1", "url": "https://amazon.com/", "content": "SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1", "ups": "100", "downs": "10"},
         "2": {"id": "2", "title": "Second book", "author": "Author2", "url": "https://amazon.com", "content": "SAMPLE TEXT AUTHOR 2 SAMPLE TEXT AUTHOR 2 SAMPLE TEXT", "ups": "100", "downs": "10"},
         "3": {"id": "3", "title": "Third book", "author": "Author3", "url": null, "content": null, "ups": null, "downs": null },
         "4": {"id": "4", "title": "Fourth book", "author": "Author4", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4", "ups": "1000", "downs": "0"},
         "5": {"id": "5", "title": "Fifth book", "author": "Author5", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT", "ups": "50", "downs": "0"} };

    var relatedPosts = {
        "1": [posts['4']],
        "2": [posts['3'], posts['5']],
        "3": [posts['2'], posts['1']],
        "4": [posts['2'], posts['1']],
        "5": []
    };

    console.log("Got a BatchInvoke Request. The payload has %d items to resolve.", event.length);
    // event is now an array
    var field = event[0].field;
    switch(field) {
        case "relatedPosts":
            var results = [];
            results.push({ 'data': relatedPosts['1'] });
            results.push({ 'data': relatedPosts['2'] });
            results.push({ 'data': null, 'errorMessage': 'Error Happened', 'errorType': 'ERROR' });
            results.push(null);
            results.push({ 'data': relatedPosts['3'], 'errorMessage': 'Error Happened with last result', 'errorType': 'ERROR' });
            callback(null, results);
            break;
        default:
            callback("Unknown field, unable to resolve" + field, null);
            break;
    }
};
```

对于该示例，以下响应映射模板解析 Lambda 函数的每个项目，并引发发生的任何错误：

```
#if( $context.result && $context.result.errorMessage )
    $utils.error($context.result.errorMessage, $context.result.errorType, $context.result.data)
#else
    $utils.toJson($context.result.data)
#end
```

此示例返回与以下内容类似的 GraphQL 响应：

```
{
  "data": {
    "allPosts": [
      {
        "id": "1",
        "relatedPostsPartialErrors": [
          {
            "id": "4",
            "title": "Fourth book"
          }
        ]
      },
      {
        "id": "2",
        "relatedPostsPartialErrors": [
          {
            "id": "3",
            "title": "Third book"
          },
          {
            "id": "5",
            "title": "Fifth book"
          }
        ]
      },
      {
        "id": "3",
        "relatedPostsPartialErrors": null
      },
      {
        "id": "4",
        "relatedPostsPartialErrors": null
      },
      {
        "id": "5",
        "relatedPostsPartialErrors": null
      }
    ]
  },
  "errors": [
    {
      "path": [
        "allPosts",
        2,
        "relatedPostsPartialErrors"
      ],
      "errorType": "ERROR",
      "locations": [
        {
          "line": 4,
          "column": 9
        }
      ],
      "message": "Error Happened"
    },
    {
      "path": [
        "allPosts",
        4,
        "relatedPostsPartialErrors"
      ],
      "data": [
        {
          "id": "2",
          "title": "Second book"
        },
        {
          "id": "1",
          "title": "First book"
        }
      ],
      "errorType": "ERROR",
      "locations": [
        {
          "line": 4,
          "column": 9
        }
      ],
      "message": "Error Happened with last result"
    }
  ]
}
```

### 配置最大批处理大小


默认情况下，使用时`BatchInvoke`， Amazon AppSync 会将请求分批发送到您的 Lambda 函数，每批最多五项。您可以配置 Lambda 解析器的最大批次大小。

要在解析器上配置最大批处理大小，请在 Amazon Command Line Interface ()Amazon CLI中使用以下命令：

```
$ aws appsync create-resolver --api-id <api-id> --type-name Query --field-name relatedPosts \
 --request-mapping-template "<template>" --response-mapping-template "<template>" --data-source-name "<lambda-datasource>" \ 
 --max-batch-size X
```

**注意**  
在提供请求映射模板时，您必须使用 `BatchInvoke` 操作才能使用批处理。

您也可以使用以下命令，在直接 Lambda 解析器上启用和配置批处理：

```
$ aws appsync create-resolver --api-id <api-id> --type-name Query --field-name relatedPosts \
 --data-source-name "<lambda-datasource>" \ 
 --max-batch-size X
```

### 使用 VTL 模板配置最大批处理大小


对于具有 VTL 请求中模板的 Lambda 解析器，最大批次大小无效，除非它们在 VTL 中直接将其指定为 `BatchInvoke` 操作。同样，如果您执行顶级变更，则不会为变更执行批处理，因为 GraphQL 规范要求按顺序执行并行变更。

例如，采用以下变更：

```
type Mutation {
    putItem(input: Item): Item
    putItems(inputs: [Item]): [Item]
}
```

通过使用第一个变更，我们可以创建 10 个 `Items`，如下面的代码片段所示：

```
mutation MyMutation {
    v1: putItem($someItem1) {
        id,
        name
    }
    v2: putItem($someItem2) {
        id,
        name
    }
    v3: putItem($someItem3) {
        id,
        name
    } 
    v4: putItem($someItem4) {
        id,
        name
    }
    v5: putItem($someItem5) {
        id,
        name
    }
    v6: putItem($someItem6) {
        id,
        name
    } 
    v7: putItem($someItem7) {
        id,
        name
    }
    v8: putItem($someItem8) {
        id,
        name
    }
    v9: putItem($someItem9) {
        id,
        name
    }
    v10: putItem($someItem10) {
        id,
        name
    }
}
```

在该示例中，即使在 Lambda 解析器中将最大批次大小设置为 10，也不会以 10 为一组对 `Items` 进行批处理，而是根据 GraphQL 规范按顺序执行它们。

要执行实际的批处理变更，您可以按照以下示例使用第二个变更：

```
mutation MyMutation {
    putItems([$someItem1, $someItem2, $someItem3,$someItem4, $someItem5, $someItem6, 
    $someItem7, $someItem8, $someItem9, $someItem10]) {
    id,
    name
    }
}
```

有关使用直接 Lambda 解析器进行批处理的更多信息，请参阅[直接 Lambda 解析器](resolver-mapping-template-reference-lambda.md#direct-lambda-resolvers)。

# 在中使用亚马逊 OpenSearch 服务解析器 Amazon AppSync
使用 OpenSearch 服务解析器

**注意**  
我们现在主要支持 APPSYNC\$1JS 运行时系统及其文档。请考虑使用 APPSYNC\$1JS 运行时系统和[此处](https://docs.amazonaws.cn/appsync/latest/devguide/tutorials-js.html)的指南。

Amazon AppSync 支持从您在自己的 Amazon 账户中配置的域中使用 Amazon OpenSearch 服务，前提是这些域不存在于 VPC 中。预置域后，您可以使用数据来源连接这些域，此时，您可以在架构中配置一个解析器以执行 GraphQL 操作（如查询、变更和订阅）。本教程将引导您了解一些常见示例。

有关更多信息，请参阅[解析器映射模板参考。 OpenSearch](resolver-mapping-template-reference-elasticsearch.md#aws-appsync-resolver-mapping-template-reference-elasticsearch)

## 一键设置


要在配置了 Amazon AppSync 亚马逊 OpenSearch 服务的情况下自动设置 GraphQL 终端节点，您可以使用以下 Amazon CloudFormation 模板：

[https://console.amazonaws.cn/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3.us-west-2.amazonaws.com/awsappsync/resources/elasticsearch/appsynces.yml](https://console.amazonaws.cn/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3.us-west-2.amazonaws.com/awsappsync/resources/elasticsearch/appsynces.yml)

 Amazon CloudFormation 部署完成后，您可以直接跳到[运行 GraphQL 查询和](#tutorial-elasticsearch-resolvers-perform-queries-mutations)突变。

## 创建新的 OpenSearch 服务域


要开始使用本教程，您需要一个现有的 OpenSearch 服务域。如果您没有域，可以使用以下示例。请注意，创建 OpenSearch 服务域最多可能需要 15 分钟，然后才能继续将其与 Amazon AppSync数据源集成。

```
aws cloudformation create-stack --stack-name AppSyncOpenSearch \
--template-url https://s3.us-west-2.amazonaws.com/awsappsync/resources/elasticsearch/ESResolverCFTemplate.yaml \
--parameters ParameterKey=OSDomainName,ParameterValue=ddtestdomain ParameterKey=Tier,ParameterValue=development \
--capabilities CAPABILITY_NAMED_IAM
```

您可以在自己的 Amazon 账户中在美国西部 2（俄勒冈）地区启动以下 Amazon CloudFormation 堆栈：

 [https://console.amazonaws.cn/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3.us-west-2.amazonaws.com/awsappsync/resources/elasticsearch/ESResolverCFTemplate.yaml](https://console.amazonaws.cn/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3.us-west-2.amazonaws.com/awsappsync/resources/elasticsearch/ESResolverCFTemplate.yaml)

## 为 OpenSearch 服务配置数据源


创建 OpenSearch 服务域后，导航到您的 Amazon AppSync GraphQL API，然后选择 “**数据源**” 选项卡。选择**新建**，并为数据来源输入一个友好名称，例如“oss”。然后选择 **Amazon OpenSearch 域**作为**数据源类型**，选择相应的区域，您应该会看到您的 OpenSearch 服务域已列出。选择后，您可以创建一个新角色并 Amazon AppSync 分配适合角色的权限，也可以选择具有以下内联策略的现有角色：

------
#### [ JSON ]

****  

```
{
    "Version":"2012-10-17",		 	 	 
    "Statement": [
        {
            "Sid": "Stmt1234234",
            "Effect": "Allow",
            "Action": [
                "es:ESHttpDelete",
                "es:ESHttpHead",
                "es:ESHttpGet",
                "es:ESHttpPost",
                "es:ESHttpPut"
            ],
            "Resource": [
                "arn:aws:es:us-east-1:111122223333:domain/democluster/*"
            ]
        }
    ]
}
```

------

您还需要 Amazon AppSync 为该角色与建立信任关系：

------
#### [ JSON ]

****  

```
{
    "Version":"2012-10-17",		 	 	 
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "appsync.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
```

------

此外， OpenSearch 服务域有自己的**访问策略**，您可以通过 Amazon Ser OpenSearch vice 控制台对其进行修改。您需要添加类似于以下内容的策略，其中包含适用于 OpenSearch 服务域的相应操作和资源。请注意，**委托**人将是 AppSync 数据源角色，如果您让控制台创建该角色，则可以在 IAM 控制台中找到该角色。

------
#### [ JSON ]

****  

```
{
    "Version":"2012-10-17",		 	 	 
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::111122223333:role/service-role/APPSYNC_DATASOURCE_ROLE"
            },
            "Action": [
                "es:ESHttpDelete",
                "es:ESHttpHead",
                "es:ESHttpGet",
                "es:ESHttpPost",
                "es:ESHttpPut"
            ],
            "Resource": "arn:aws:es:us-east-1:111122223333:domain/DOMAIN_NAME/*"
        }
    ]
}
```

------

## 连接解析器


现在，数据源已连接到您的 OpenSearch 服务域，您可以使用解析器将其连接到您的 GraphQL 架构，如以下示例所示：

```
 schema {
   query: Query
   mutation: Mutation
 }

 type Query {
   getPost(id: ID!): Post
   allPosts: [Post]
 }

 type Mutation {
   addPost(id: ID!, author: String, title: String, url: String, ups: Int, downs: Int, content: String): AWSJSON
 }

type Post {
  id: ID!
  author: String
  title: String
  url: String
  ups: Int
  downs: Int
  content: String
}
...
```

请注意，有一个用户定义的 `Post` 类型（具有一个 `id` 字段）。在以下示例中，我们假设有一个过程（可以自动化）将此类型放入您的 OpenSearch 服务域，该过程将映射到的路径根目录`/post/_doc`，索引在`post`哪里。从该根路径中，您可以执行单独文档搜索、使用 `/id/post*` 的通配符搜索或使用 `/post/_search` 路径的多文档搜索。例如，如果您具有另一个名为 `User` 的类型，您可以使用名为 `user` 的新索引对文档编制索引，然后使用 `/user/_search` **路径**执行搜索。

在 Amazon AppSync 控制台的架构编辑器中，修改前面的`Posts`架构以包含`searchPosts`查询：

```
type Query {
  getPost(id: ID!): Post
  allPosts: [Post]
  searchPosts: [Post]
}
```

保存架构。在右侧，对于 `searchPosts`，选择 **Attach resolver (附加解析器)**。在**操作**菜单中，选择**更新运行时**，然后选择**单位解析器 (仅限 VTL)**。然后，选择您的 OpenSearch 服务数据源。在**请求映射模板**部分，选择**查询文章**的下拉列表以获取基本模板。将 `path` 修改为 `/post/_search`。它应该类似以下内容：

```
{
    "version":"2017-02-28",
    "operation":"GET",
    "path":"/post/_search",
    "params":{
        "headers":{},
        "queryString":{},
        "body":{
            "from":0,
            "size":50
        }
    }
}
```

这假设前面的架构中包含已在 S OpenSearch ervice 中为该`post`字段编制索引的文档。如果您以不同方式设置数据的结构，将需要相应地进行更新。

在**响应映射模板**部分下，如果要从 OpenSearch 服务查询中取回数据结果并转换为 GraphQL，则需要指定相应的`_source`过滤器。使用以下模板：

```
[
    #foreach($entry in $context.result.hits.hits)
    #if( $velocityCount > 1 ) , #end
    $utils.toJson($entry.get("_source"))
    #end
]
```

## 修改您的搜索


上述的请求映射模板针对所有记录执行一个简单查询。假设您想要按某个特定作者进行搜索。此外，假设您希望该作者是在 GraphQL 查询中定义的一个参数。在 Amazon AppSync 控制台的架构编辑器中，添加一个 `allPostsByAuthor` 查询：

```
type Query {
  getPost(id: ID!): Post
  allPosts: [Post]
  allPostsByAuthor(author: String!): [Post]
  searchPosts: [Post]
}
```

现在选择 Att **ach resolver** 并选择 OpenSearch 服务数据源，但在**响应映射模板**中使用以下示例：

```
{
    "version":"2017-02-28",
    "operation":"GET",
    "path":"/post/_search",
    "params":{
        "headers":{},
        "queryString":{},
        "body":{
            "from":0,
            "size":50,
            "query":{
                "match" :{
                    "author": $util.toJson($context.arguments.author)
                }
            }
        }
    }
}
```

请注意，`body` 用一个针对 `author` 字段的术语查询填充，它将作为一个参数从客户端进行传递。您可以选择已预填充信息（如标准文本），甚至使用其他[实用程序](resolver-context-reference.md#aws-appsync-resolver-mapping-template-context-reference)。

如果您使用此解析器，则使用上例所示的相同信息填写**响应映射模板**。

## 向 OpenSearch 服务添加数据


由于 GraphQL 突变，您可能需要向 OpenSearch 服务域中添加数据。这是一个用于搜索和其他用途的强大机制。由于您可以使用 GraphQL 订阅来[实现实时数据](aws-appsync-real-time-data.md)，因此它可以用作通知客户服务域中数据 OpenSearch 更新的机制。

返回 Amazon AppSync 控制台的 “**架构**” 页面，为`addPost()`突变选择**附加解析器**。再次选择 OpenSearch 服务数据源，然后为`Posts`架构使用以下**响应映射模板**：

```
{
    "version":"2017-02-28",
    "operation":"PUT",
    "path": $util.toJson("/post/_doc/$context.arguments.id"),
    "params":{
        "headers":{},
        "queryString":{},
        "body":{
            "id": $util.toJson($context.arguments.id),
            "author": $util.toJson($context.arguments.author),
            "ups": $util.toJson($context.arguments.ups),
            "downs": $util.toJson($context.arguments.downs),
            "url": $util.toJson($context.arguments.url),
            "content": $util.toJson($context.arguments.content),
            "title": $util.toJson($context.arguments.title)
        }
    }
}
```

和之前一样，这是一个介绍如何设置数据的结构的示例。如果您有不同的字段名称或索引，则需要相应地更新 `path` 和 `body`。此示例还显示如何使用 `$context.arguments` 从您的 GraphQL 变更参数填充模板。

在继续之前，使用以下响应映射模板，这会返回变更操作结果或错误信息以作为输出：

```
#if($context.error)
    $util.toJson($ctx.error)
#else
    $util.toJson($context.result)
#end
```

## 检索单个文档


最后，如果您想在您的架构中使用 `getPost(id:ID)` 查询以返回单个文档，请在 Amazon AppSync 控制台的架构编辑器中找到此查询，然后选择 **Attach resolver (附加解析程序)**。再次选择 OpenSearch 服务数据源并使用以下映射模板：

```
{
    "version":"2017-02-28",
    "operation":"GET",
    "path": $util.toJson("post/_doc/$context.arguments.id"),
    "params":{
        "headers":{},
        "queryString":{},
        "body":{}
    }
}
```

由于上面的 `path` 将 `id` 参数用于空正文，此命令将返回单个文档。但是，您需要使用以下响应映射模板，因为现在您返回的是单个项目而不是列表：

```
$utils.toJson($context.result.get("_source"))
```

## 执行查询和变更


现在，您应该能够对您的 OpenSearch 服务域执行 GraphQL 操作了。导航到 Amazon AppSync 控制台的 “**查询**” 选项卡并添加一条新记录：

```
mutation addPost {
    addPost (
        id:"12345"
        author: "Fred"
        title: "My first book"
        content: "This will be fun to write!"
        url: "publisher website",
        ups: 100,
        downs:20 
       )
}
```

您将会在右侧看到变更结果。同样，您现在可以对您的 OpenSearch 服务域运行`searchPosts`查询：

```
query searchPosts {
    searchPosts {
        id
        title
        author
        content
    }
}
```

## 最佳实践

+ OpenSearch 服务应该用于查询数据，而不是作为您的主数据库。如组合 GraphQL 解 OpenSearch 析器中所述，您可能需要将服务与亚马逊 DynamoDB [结合](tutorial-combining-graphql-resolvers.md#aws-appsync-tutorial-combining-graphql-resolvers)使用。
+ 仅通过允许 Amazon AppSync 服务角色访问集群来授予对域的访问权限。
+ 您可以通过最低成本的集群先开始小规模开发，然后随着您转向生产阶段，而转至具有高可用性 (HA) 的较大集群。

# 在 Amazon AppSync 中使用本地解析器
使用本地解析器

**注意**  
我们现在主要支持 APPSYNC\$1JS 运行时系统及其文档。请考虑使用 APPSYNC\$1JS 运行时系统和[此处](https://docs.amazonaws.cn/appsync/latest/devguide/tutorials-js.html)的指南。

Amazon AppSync 允许您使用支持的数据来源（Amazon Lambda、Amazon DynamoDB 或 Amazon OpenSearch Service）执行各种操作。但在某些情况下，可能不必调用支持的数据来源。

这时本地解析器就很方便。本地解析器无需调用远程数据来源，只需将请求映射模板的结果**转发**到响应映射模板。字段解析是在 Amazon AppSync 中完成的。

在许多使用案例中本地解析器都很有用。最常用的使用案例是在不触发数据来源调用的情况下发布通知。要演示此使用案例，让我们构建一个传呼应用程序，用户可互相呼叫。此示例利用了*订阅*，如果您不熟悉*订阅*，可以参考[实时数据](aws-appsync-real-time-data.md)教程。

## 创建呼叫应用程序


在我们的呼叫应用程序中，客户端可以订阅收件箱，并向其他客户端发送呼叫。每个呼叫包含一条消息。以下是架构：

```
schema {
    query: Query
    mutation: Mutation
    subscription: Subscription
}

type Subscription {
    inbox(to: String!): Page
    @aws_subscribe(mutations: ["page"])
}

type Mutation {
    page(body: String!, to: String!): Page!
}

type Page {
    from: String
    to: String!
    body: String!
    sentAt: String!
}

type Query {
    me: String
}
```

让我们在 `Mutation.page` 字段上附加一个解析器。在 **Schema (架构)** 窗格中，单击右侧面板中字段定义旁的 *Attach Resolver (附加解析器)*。创建类型为 *None* 的新数据来源，并命名为 *PageDataSource*。

在请求映射模板中输入：

```
{
  "version": "2017-02-28",
  "payload": {
    "body": $util.toJson($context.arguments.body),
    "from": $util.toJson($context.identity.username),
    "to":  $util.toJson($context.arguments.to),
    "sentAt": "$util.time.nowISO8601()"
  }
}
```

对于响应映射模板，选择默认的 *Forward the result (转发结果)*。保存解析器。您的应用程序现在已经就绪，让我们开始呼叫吧！

## 发送呼叫和订阅呼叫


接收呼叫的客户端必须首先订阅收件箱。

让我们在 **Queries (查询)** 窗格中执行 `inbox` 订阅：

```
subscription Inbox {
    inbox(to: "Nadia") {
        body
        to
        from
        sentAt
    }
}
```

 每次调用 `Mutation.page` 变更时，*Nadia* 都会收到页面。执行变更可进行调用：

```
mutation Page {
    page(to: "Nadia", body: "Hello, World!") {
        body
        to
        from
        sentAt
    }
}
```

我们刚刚说明了如何使用本地解析器，即，在不离开 Amazon AppSync 的情况下发送和接收页面。

# 将 GraphQL 解析器组合在一起 Amazon AppSync
组合使用 GraphQL 解析器

**注意**  
我们现在主要支持 APPSYNC\$1JS 运行时系统及其文档。请考虑使用 APPSYNC\$1JS 运行时系统和[此处](https://docs.amazonaws.cn/appsync/latest/devguide/tutorials-js.html)的指南。

GraphQL 架构中的解析器和字段具有 1:1 的关系，具有很高的灵活性。由于数据来源是独立于架构在解析器上配置的，因此您可以通过不同的数据来源解析和操控 GraphQL 类型，同时在架构上混合和匹配数据来源以最好地满足您的需要。

以下示例场景说明了如何在架构中混合使用和匹配数据来源。在开始之前，我们建议您熟悉如何为 Amazon Lambda Amazon DynamoDB和亚马逊服务设置数据源 OpenSearch 和解析器，如前面的教程所述。

## 示例架构


以下架构具有一个名为 `Post` 的类型，它定义了 3 个 `Query` 操作和 3 个 `Mutation` 操作：

```
type Post {
    id: ID!
    author: String!
    title: String
    content: String
    url: String
    ups: Int
    downs: Int
    version: Int!
}

type Query {
    allPost: [Post]
    getPost(id: ID!): Post
    searchPosts: [Post]
}

type Mutation {
    addPost(
        id: ID!,
        author: String!,
        title: String,
        content: String,
        url: String
    ): Post
    updatePost(
        id: ID!,
        author: String!,
        title: String,
        content: String,
        url: String,
        ups: Int!,
        downs: Int!,
        expectedVersion: Int!
    ): Post
    deletePost(id: ID!): Post
}
```

在本示例中，您共有 6 个可附加的解析器。一种可能的方法是，让所有这些解析器来自名为 `Posts` 的 Amazon DynamoDB 表，其中 `AllPosts` 运行扫描，`searchPosts` 运行查询，如 [DynamoDB 解析器映射模板参考](resolver-mapping-template-reference-dynamodb.md#aws-appsync-resolver-mapping-template-reference-dynamodb)中所述。但是，还有其他方法可以满足您的业务需求，例如让这些 GraphQL 查询从 Lambda 或服务中解析。 OpenSearch 

## 通过解析程序更改数据


您可能需要将结果从数据库（例如 DynamoDB 或 Amazon Aurora）返回到客户端，并更改某些属性。这可能是由于数据类型的格式设置所致（例如，客户端上的时间戳差异），或者是为了处理向后兼容性问题。出于说明目的，在以下示例中，每次调用 GraphQL 解析器时， Amazon Lambda 函数为其分配随机数以处理博客文章的点赞和差评操作：

```
'use strict';
const doc = require('dynamodb-doc');
const dynamo = new doc.DynamoDB();

exports.handler = (event, context, callback) => {
    const payload = {
        TableName: 'Posts',
        Limit: 50,
        Select: 'ALL_ATTRIBUTES',
    };

    dynamo.scan(payload, (err, data) => {
        const result = { data: data.Items.map(item =>{
            item.ups = parseInt(Math.random() * (50 - 10) + 10, 10);
            item.downs = parseInt(Math.random() * (20 - 0) + 0, 10);
            return item;
        }) };
        callback(err, result.data);
    });
};
```

这是一个完全有效的 Lambda 函数，可以附加到 GraphQL 架构中的 `AllPosts` 字段，以便返回所有结果的任何查询都可以对顶/踩操作获得随机数字。

## DynamoDB 和服务 OpenSearch


对于某些应用程序，您可以对 DynamoDB 执行变更或简单查找查询，并让后台进程将文档传输到服务。 OpenSearch 然后，您只需将`searchPosts`解析器附加到 OpenSearch 服务数据源，然后使用 GraphQL 查询返回搜索结果（来自源自 DynamoDB 的数据）。在应用程序中添加高级搜索操作（例如关键字、模糊字词匹配，甚至地理空间查找）时，这可能是非常强大的。可以通过 ETL 流程完成从 DynamoDB 传输数据的过程，也可以使用 Lambda 从 DynamoDB 进行流式传输。您可以使用 Amazon 账户中位于美国西部 2（俄勒冈）地区的以下 Amazon CloudFormation 堆栈启动一个完整的示例：

[https://console.amazonaws.cn/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3.us-west-2.amazonaws.com/awsappsync/resources/multipledatasource/appsyncesdbstream.yml](https://console.amazonaws.cn/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3.us-west-2.amazonaws.com/awsappsync/resources/multipledatasource/appsyncesdbstream.yml) 

该示例中的架构允许您使用 DynamoDB 解析器添加文章，如下所示：

```
mutation add {
    putPost(author:"Nadia"
        title:"My first post"
        content:"This is some test content"
        url:"https://aws.amazon.com/appsync/"
    ){
        id
        title
    }
}
```

这会将数据写入 DynamoDB，DynamoDB 然后通过 Lambda 将数据流式传输到 OpenSearch 亚马逊服务，您可以按不同的字段搜索所有帖子。例如，由于数据存储在 Amazon S OpenSearch ervice 中，因此您可以使用自由格式的文本（即使是空格）搜索作者字段或内容字段，如下所示：

```
query searchName{
    searchAuthor(name:"   Nadia   "){
        id
        title
        content
    }
}

query searchContent{
    searchContent(text:"test"){
        id
        title
        content
    }
}
```

由于数据直接写入到 DynamoDB 中，因此，您仍然可以使用 `allPosts{...}` 和 `singlePost{...}` 查询对表执行高效的列表或项目查找操作。该堆栈将以下示例代码用于 DynamoDB 流：

 **注意**：此代码仅用于举例说明。

```
var AWS = require('aws-sdk');
var path = require('path');
var stream = require('stream');

var esDomain = {
    endpoint: 'https://opensearch-domain-name.REGION.es.amazonaws.com',
    region: 'REGION',
    index: 'id',
    doctype: 'post'
};

var endpoint = new AWS.Endpoint(esDomain.endpoint)
var creds = new AWS.EnvironmentCredentials('AWS');

function postDocumentToES(doc, context) {
    var req = new AWS.HttpRequest(endpoint);

    req.method = 'POST';
    req.path = '/_bulk';
    req.region = esDomain.region;
    req.body = doc;
    req.headers['presigned-expires'] = false;
    req.headers['Host'] = endpoint.host;

    // Sign the request (Sigv4)
    var signer = new AWS.Signers.V4(req, 'es');
    signer.addAuthorization(creds, new Date());

    // Post document to ES
    var send = new AWS.NodeHttpClient();
    send.handleRequest(req, null, function (httpResp) {
        var body = '';
        httpResp.on('data', function (chunk) {
            body += chunk;
        });
        httpResp.on('end', function (chunk) {
            console.log('Successful', body);
            context.succeed();
        });
    }, function (err) {
        console.log('Error: ' + err);
        context.fail();
    });
}

exports.handler = (event, context, callback) => {
    console.log("event => " + JSON.stringify(event));
    var posts = '';

    for (var i = 0; i < event.Records.length; i++) {
        var eventName = event.Records[i].eventName;
        var actionType = '';
        var image;
        var noDoc = false;
        switch (eventName) {
            case 'INSERT':
                actionType = 'create';
                image = event.Records[i].dynamodb.NewImage;
                break;
            case 'MODIFY':
                actionType = 'update';
                image = event.Records[i].dynamodb.NewImage;
                break;
            case 'REMOVE':
            actionType = 'delete';
                image = event.Records[i].dynamodb.OldImage;
                noDoc = true;
                break;
        }

        if (typeof image !== "undefined") {
            var postData = {};
            for (var key in image) {
                if (image.hasOwnProperty(key)) {
                    if (key === 'postId') {
                        postData['id'] = image[key].S;
                    } else {
                        var val = image[key];
                        if (val.hasOwnProperty('S')) {
                            postData[key] = val.S;
                        } else if (val.hasOwnProperty('N')) {
                            postData[key] = val.N;
                        }
                    }
                }
            }

            var action = {};
            action[actionType] = {};
            action[actionType]._index = 'id';
            action[actionType]._type = 'post';
            action[actionType]._id = postData['id'];
            posts += [
                JSON.stringify(action),
            ].concat(noDoc?[]:[JSON.stringify(postData)]).join('\n') + '\n';
        }
    }
    console.log('posts:',posts);
    postDocumentToES(posts, context);
};
```

然后，您可以使用 DynamoDB 流将其附加到主键`id`为的 DynamoDB 表，并且对 DynamoDB 源的任何更改都将流式传输到您的服务域中。 OpenSearch 有关配置上述功能的更多信息，请参阅 [DynamoDB 流文档](https://docs.amazonaws.cn/amazondynamodb/latest/developerguide/Streams.Lambda.html)。

# 在中使用 DynamoDB 批量操作 Amazon AppSync
执行 DynamoDB 批处理操作

**注意**  
我们现在主要支持 APPSYNC\$1JS 运行时系统及其文档。请考虑使用 APPSYNC\$1JS 运行时系统和[此处](https://docs.amazonaws.cn/appsync/latest/devguide/tutorials-js.html)的指南。

Amazon AppSync 支持对单个区域中的一个或多个表使用 Amazon DynamoDB 批量操作。支持的操作为 `BatchGetItem`、`BatchPutItem` 和 `BatchDeleteItem`。通过在 Amazon AppSync 中使用这些功能，您可以执行如下任务：
+ 在单个查询中传递键列表，并从表中返回结果
+ 在单个查询中从一个或多个表读取记录
+ 将记录批量写入一个或多个表
+ 有条件写入或删除多个可能有关系的表中的记录

在 DynamoDB Amazon AppSync 中使用批处理操作是一种高级技术，需要对后端操作和表结构多加思考和了解。此外，中的批处理操作与非批处理操作 Amazon AppSync 有两个主要区别：
+ 数据来源角色必须对解析器将访问的所有表具有权限。
+ 解析器的表规范是映射模板的一部分。

## Permissions


与其他解析器一样，您需要在中创建数据源， Amazon AppSync 然后创建角色或使用现有角色。由于批处理操作需要具有 DynamoDB 表的不同权限，因此，您需要为配置的角色授予读取或写入操作权限：

------
#### [ JSON ]

****  

```
{
    "Version":"2012-10-17",		 	 	 
    "Statement": [
        {
            "Action": [
                "dynamodb:BatchGetItem",
                "dynamodb:BatchWriteItem"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:dynamodb:us-east-1:111122223333:table/TABLENAME",
                "arn:aws:dynamodb:us-east-1:111122223333:table/TABLENAME/*"
            ]
        }
    ]
}
```

------

 **注意**：角色与中的数据源相关联 Amazon AppSync，字段上的解析器是针对数据源调用的。配置为针对 DynamoDB 获取的数据来源仅指定一个表，以使配置保持简单。因此，当在单个解析器中针对多个表执行批处理操作（这是一项更高级的任务）时，您必须向该数据来源的角色授予对将与解析器进行交互的任何表的访问权限。这项操作将在上面 IAM 策略中的 **Resource (资源)** 字段中执行。对表进行配置以便对它们进行批处理调用是在解析器模板中实现的，下面将介绍相关内容。

## 数据来源


为简便起见，我们将为本教程中使用的所有解析器使用同一个数据来源。在**数据源**选项卡上，创建一个新的 DynamoDB 数据源并将其命名。**BatchTutorial**表名称可以是任何内容，因为表名称被指定为批处理操作的请求映射模板的一部分。我们将提供表名称 `empty`。

对于本教程，具有以下内联策略的任何角色都有效：

## 单个表批处理


**警告**  
与冲突检测和解决功能一起使用时不支持 `BatchPutItem` 和 `BatchDeleteItem`。必须禁用这些设置以防止可能出现的错误。

在此示例中，假设您有一个名为 **Posts** 的表，您要通过批处理操作在其中添加和删除项目。使用以下架构，请注意，对于查询，我们将传入一个列表 IDs：

```
type Post {
    id: ID!
    title: String
}

input PostInput {
    id: ID!
    title: String
}

type Query {
    batchGet(ids: [ID]): [Post]
}

type Mutation {
    batchAdd(posts: [PostInput]): [Post]
    batchDelete(ids: [ID]): [Post]
}

schema {
    query: Query
    mutation: Mutation
}
```

使用以下**请求映射模板**将一个解析器附加到 `batchAdd()` 字段。这会自动接受 GraphQL `input PostInput` 类型的每个项目并构建 `BatchPutItem` 操作所需的一个映射：

```
#set($postsdata = [])
#foreach($item in ${ctx.args.posts})
    $util.qr($postsdata.add($util.dynamodb.toMapValues($item)))
#end

{
    "version" : "2018-05-29",
    "operation" : "BatchPutItem",
    "tables" : {
        "Posts": $utils.toJson($postsdata)
    }
}
```

在这种情况下，**响应映射模板**是一个简单的传递，但表名称作为 `..data.Posts` 追加到上下文对象，如下所示：

```
$util.toJson($ctx.result.data.Posts)
```

现在，导航到 ** 控制台的**查询 Amazon AppSync页面并运行以下 **batchAdd** 更改：

```
mutation add {
    batchAdd(posts:[{
            id: 1 title: "Running in the Park"},{
            id: 2 title: "Playing fetch"
        }]){
            id
            title
    }
}
```

您应该会看到输出到屏幕的结果，并且可以通过 DynamoDB 控制台单独验证这两个值是否写入到 **Posts** 表。

接下来，使用以下**请求映射模板**将一个解析器附加到 `batchGet()` 字段。这会自动接受 GraphQL `ids:[]` 类型的每个项目并构建 `BatchGetItem` 操作所需的一个映射：

```
#set($ids = [])
#foreach($id in ${ctx.args.ids})
    #set($map = {})
    $util.qr($map.put("id", $util.dynamodb.toString($id)))
    $util.qr($ids.add($map))
#end

{
    "version" : "2018-05-29",
    "operation" : "BatchGetItem",
    "tables" : {
        "Posts": {
            "keys": $util.toJson($ids),
            "consistentRead": true,
            "projection" : {
                "expression" : "#id, title",
                "expressionNames" : { "#id" : "id"}
                }
        }
    }
}
```

**响应映射模板**还是一个简单的传递，且再一次，表名称作为 `..data.Posts` 追加到上下文对象：

```
$util.toJson($ctx.result.data.Posts)
```

现在，回到 ** 控制台的**查询 Amazon AppSync页面并运行以下 **batchGet 查询**：

```
query get {
    batchGet(ids:[1,2,3]){
        id
        title
    }
}
```

这应返回您早前添加的两个 `id` 值的结果。请注意，对于值为 `null` 的 `id`，将返回 `3` 值。这是因为您的 **Posts** 表中还没有具有该值的记录。另请注意， Amazon AppSync返回结果的顺序与传递给查询的密钥的顺序相同，这是一项代表您 Amazon AppSync 执行的附加功能。因此，如果您切换到 `batchGet(ids:[1,3,2)`，将看到订单已更改。您还将了解哪个 `id` 返回了 `null` 值。

最后，使用以下**请求映射模板**将一个解析器附加到 `batchDelete()` 字段。这会自动接受 GraphQL `ids:[]` 类型的每个项目并构建 `BatchGetItem` 操作所需的一个映射：

```
#set($ids = [])
#foreach($id in ${ctx.args.ids})
    #set($map = {})
    $util.qr($map.put("id", $util.dynamodb.toString($id)))
    $util.qr($ids.add($map))
#end

{
    "version" : "2018-05-29",
    "operation" : "BatchDeleteItem",
    "tables" : {
        "Posts": $util.toJson($ids)
    }
}
```

**响应映射模板**还是一个简单的传递，且再一次，表名称作为 `..data.Posts` 追加到上下文对象：

```
$util.toJson($ctx.result.data.Posts)
```

现在，回到 ** 控制台的**查询 Amazon AppSync页面并运行以下 **batchDelete** 更改：

```
mutation delete {
    batchDelete(ids:[1,2]){ id }
}
```

现在应删除 `id` 为 `1` 和 `2` 的记录。如果您更早重新运行 `batchGet()` 查询，则应返回 `null`。

## 多个表批处理


**警告**  
与冲突检测和解决功能一起使用时不支持 `BatchPutItem` 和 `BatchDeleteItem`。必须禁用这些设置以防止可能出现的错误。

Amazon AppSync 还允许您对表执行批量操作。我们来构建更复杂的应用程序。想象一下，我们正在构建一个 Pet Health 应用程序，其中的传感器报告宠物位置和身体温度。传感器由电池供电，并每隔几分钟尝试连接到网络。当传感器建立连接时，它会将其读数发送到我们 Amazon AppSync 的 API。然后，触发器分析数据，这样，就可以向宠物主人显示控制面板。我们重点关注如何表示传感器与后端数据存储之间的交互。

作为先决条件，让我们先创建两个 DynamoDB 表；**locationReadings** 存储传感器位置读数，**temperatureReadings** 存储传感器温度读数。这两个表正好共享相同的主键结构：`sensorId (String)` 是分区键，而 `timestamp (String)` 是排序键。

我们使用以下 GraphQL 架构：

```
type Mutation {
    # Register a batch of readings
    recordReadings(tempReadings: [TemperatureReadingInput], locReadings: [LocationReadingInput]): RecordResult
    # Delete a batch of readings
    deleteReadings(tempReadings: [TemperatureReadingInput], locReadings: [LocationReadingInput]): RecordResult
}

type Query {
    # Retrieve all possible readings recorded by a sensor at a specific time
    getReadings(sensorId: ID!, timestamp: String!): [SensorReading]
}

type RecordResult {
    temperatureReadings: [TemperatureReading]
    locationReadings: [LocationReading]
}

interface SensorReading {
    sensorId: ID!
    timestamp: String!
}

# Sensor reading representing the sensor temperature (in Fahrenheit)
type TemperatureReading implements SensorReading {
    sensorId: ID!
    timestamp: String!
    value: Float
}

# Sensor reading representing the sensor location (lat,long)
type LocationReading implements SensorReading {
    sensorId: ID!
    timestamp: String!
    lat: Float
    long: Float
}

input TemperatureReadingInput {
    sensorId: ID!
    timestamp: String
    value: Float
}

input LocationReadingInput {
    sensorId: ID!
    timestamp: String
    lat: Float
    long: Float
}
```

### BatchPutItem -记录传感器读数


我们的传感器需要能够在连接到 Internet 后立即发送其读数。GraphQL 字段 `Mutation.recordReadings` 是传感器将用来执行上述操作的 API。现在附加一个解析器以实际使用 API。

选择 `Mutation.recordReadings` 字段旁边的**附加**。在下一个屏幕上，选择本教程开始时创建的同一个 `BatchTutorial` 数据来源。

我们添加以下请求映射模板。

 **请求映射模板** 

```
## Convert tempReadings arguments to DynamoDB objects
#set($tempReadings = [])
#foreach($reading in ${ctx.args.tempReadings})
    $util.qr($tempReadings.add($util.dynamodb.toMapValues($reading)))
#end

## Convert locReadings arguments to DynamoDB objects
#set($locReadings = [])
#foreach($reading in ${ctx.args.locReadings})
    $util.qr($locReadings.add($util.dynamodb.toMapValues($reading)))
#end

{
    "version" : "2018-05-29",
    "operation" : "BatchPutItem",
    "tables" : {
        "locationReadings": $utils.toJson($locReadings),
        "temperatureReadings": $utils.toJson($tempReadings)
    }
}
```

如您所见，`BatchPutItem` 操作使我们能够指定多个表。

我们使用以下响应映射模板。

 **响应映射模板** 

```
## If there was an error with the invocation
## there might have been partial results
#if($ctx.error)
    ## Append a GraphQL error for that field in the GraphQL response
    $utils.appendError($ctx.error.message, $ctx.error.message)
#end
## Also returns data for the field in the GraphQL response
$utils.toJson($ctx.result.data)
```

使用批处理操作，可能会从调用中同时返回错误和结果。在这种情况下，我们可以自主执行一些额外的错误处理。

 **注意**：`$utils.appendError()` 的用法与 `$util.error()` 相似，主要区别是前者不中断对映射模板的评估。相反，它指示字段发生了错误，但允许评估模板，因此会将数据返回给调用方。我们建议，当您的应用程序需要返回部分结果时使用 `$utils.appendError()`。

保存解析器并导航到 Amazon AppSync 控制台的 “**查询**” 页面。现在发送一些传感器读数！

执行以下变更：

```
mutation sendReadings {
  recordReadings(
    tempReadings: [
      {sensorId: 1, value: 85.5, timestamp: "2018-02-01T17:21:05.000+08:00"},
      {sensorId: 1, value: 85.7, timestamp: "2018-02-01T17:21:06.000+08:00"},
      {sensorId: 1, value: 85.8, timestamp: "2018-02-01T17:21:07.000+08:00"},
      {sensorId: 1, value: 84.2, timestamp: "2018-02-01T17:21:08.000+08:00"},
      {sensorId: 1, value: 81.5, timestamp: "2018-02-01T17:21:09.000+08:00"}
    ]
    locReadings: [
      {sensorId: 1, lat: 47.615063, long: -122.333551, timestamp: "2018-02-01T17:21:05.000+08:00"},
      {sensorId: 1, lat: 47.615163, long: -122.333552, timestamp: "2018-02-01T17:21:06.000+08:00"}
      {sensorId: 1, lat: 47.615263, long: -122.333553, timestamp: "2018-02-01T17:21:07.000+08:00"}
      {sensorId: 1, lat: 47.615363, long: -122.333554, timestamp: "2018-02-01T17:21:08.000+08:00"}
      {sensorId: 1, lat: 47.615463, long: -122.333555, timestamp: "2018-02-01T17:21:09.000+08:00"}
    ]) {
    locationReadings {
      sensorId
      timestamp
      lat
      long
    }
    temperatureReadings {
      sensorId
      timestamp
      value
    }
  }
}
```

我们在一个变更中发送了 10 个传感器读数，并在两个表之间拆分读数。使用 DynamoDB 控制台验证是否在 **locationReadings** 和 **temperatureReadings** 表中显示数据。

### BatchDeleteItem -删除传感器读数


同样，我们也需要删除批量传感器读数。我们使用 `Mutation.deleteReadings` GraphQL 字段来实现此目的。选择 `Mutation.recordReadings` 字段旁边的**附加**。在下一个屏幕上，选择本教程开始时创建的同一个 `BatchTutorial` 数据来源。

我们使用以下请求映射模板。

 **请求映射模板** 

```
## Convert tempReadings arguments to DynamoDB primary keys
#set($tempReadings = [])
#foreach($reading in ${ctx.args.tempReadings})
    #set($pkey = {})
    $util.qr($pkey.put("sensorId", $reading.sensorId))
    $util.qr($pkey.put("timestamp", $reading.timestamp))
    $util.qr($tempReadings.add($util.dynamodb.toMapValues($pkey)))
#end

## Convert locReadings arguments to DynamoDB primary keys
#set($locReadings = [])
#foreach($reading in ${ctx.args.locReadings})
    #set($pkey = {})
    $util.qr($pkey.put("sensorId", $reading.sensorId))
    $util.qr($pkey.put("timestamp", $reading.timestamp))
    $util.qr($locReadings.add($util.dynamodb.toMapValues($pkey)))
#end

{
    "version" : "2018-05-29",
    "operation" : "BatchDeleteItem",
    "tables" : {
        "locationReadings": $utils.toJson($locReadings),
        "temperatureReadings": $utils.toJson($tempReadings)
    }
}
```

该响应映射模板与我们用于 `Mutation.recordReadings` 的模板相同。

 **响应映射模板** 

```
## If there was an error with the invocation
## there might have been partial results
#if($ctx.error)
    ## Append a GraphQL error for that field in the GraphQL response
    $utils.appendError($ctx.error.message, $ctx.error.message)
#end
## Also return data for the field in the GraphQL response
$utils.toJson($ctx.result.data)
```

保存解析器并导航到 Amazon AppSync 控制台的 “**查询**” 页面。现在，让我们删除几个传感器读数！

执行以下变更：

```
mutation deleteReadings {
  # Let's delete the first two readings we recorded
  deleteReadings(
    tempReadings: [{sensorId: 1, timestamp: "2018-02-01T17:21:05.000+08:00"}]
    locReadings: [{sensorId: 1, timestamp: "2018-02-01T17:21:05.000+08:00"}]) {
    locationReadings {
      sensorId
      timestamp
      lat
      long
    }
    temperatureReadings {
      sensorId
      timestamp
      value
    }
  }
}
```

通过 DynamoDB 控制台验证是否从 **locationReadings** 和 **temperatureReadings** 表中删除了这两个读数。

### BatchGetItem -检索读数


Pet Health 应用程序的另一个常见操作是检索特定时间点的传感器读数。我们将解析器附加到架构上的 `Query.getReadings` GraphQL 字段。选择 **Attach (附加)**，然后，在下一个屏幕上选择本教程开始时创建的同一个 `BatchTutorial` 数据来源。

我们添加以下请求映射模板。

 **请求映射模板** 

```
## Build a single DynamoDB primary key,
## as both locationReadings and tempReadings tables
## share the same primary key structure
#set($pkey = {})
$util.qr($pkey.put("sensorId", $ctx.args.sensorId))
$util.qr($pkey.put("timestamp", $ctx.args.timestamp))

{
    "version" : "2018-05-29",
    "operation" : "BatchGetItem",
    "tables" : {
        "locationReadings": {
            "keys": [$util.dynamodb.toMapValuesJson($pkey)],
            "consistentRead": true
        },
        "temperatureReadings": {
            "keys": [$util.dynamodb.toMapValuesJson($pkey)],
            "consistentRead": true
        }
    }
}
```

请注意，我们现在正在使用该**BatchGetItem**操作。

我们的响应映射模板会有点不同，因为我们选择返回 `SensorReading` 列表。我们将调用结果映射到所需的形状。

 **响应映射模板** 

```
## Merge locationReadings and temperatureReadings
## into a single list
## __typename needed as schema uses an interface
#set($sensorReadings = [])

#foreach($locReading in $ctx.result.data.locationReadings)
    $util.qr($locReading.put("__typename", "LocationReading"))
    $util.qr($sensorReadings.add($locReading))
#end

#foreach($tempReading in $ctx.result.data.temperatureReadings)
    $util.qr($tempReading.put("__typename", "TemperatureReading"))
    $util.qr($sensorReadings.add($tempReading))
#end

$util.toJson($sensorReadings)
```

保存解析器并导航到 Amazon AppSync 控制台的 “**查询**” 页面。现在，我们来检索传感器读数！

执行以下查询：

```
query getReadingsForSensorAndTime {
  # Let's retrieve the very first two readings
  getReadings(sensorId: 1, timestamp: "2018-02-01T17:21:06.000+08:00") {
    sensorId
    timestamp
    ...on TemperatureReading {
      value
    }
    ...on LocationReading {
      lat
      long
    }
  }
}
```

我们已经成功演示了使用的 DynamoDB 批处理操作的用法。 Amazon AppSync

## 错误处理


在中 Amazon AppSync，数据源操作有时会返回部分结果。部分结果是一个术语，我们用它来表示操作的输出中包含某些数据和一个错误。由于错误处理本质上是特定于应用程序的，因此 Amazon AppSync 让您有机会处理响应映射模板中的错误。上下文中的解析器调用错误（如果有）为 `$ctx.error`。调用错误始终包含一条消息和一个类型，可作为属性 `$ctx.error.message` 和 `$ctx.error.type` 进行访问。在响应映射模板调用期间，您可以通过三种方式处理部分结果：

1. 仅返回数据以忽略调用错误

1. 通过停止响应映射模板评估（这不会返回任何数据）来引发错误（使用 `$util.error(...)`）。

1. 附加一个错误（使用 `$util.appendError(...)`）并且也返回数据

让我们通过 DynamoDB 批处理操作分别说明上述三点！

### DynamoDB 批处理操作


借助 DynamoDB 批处理操作，批处理可能会部分完成。也就是说，某些请求的项目或键未得到处理。如果无法完成批处理， Amazon AppSync 则将在上下文中设置未处理的项目和调用错误。

我们将使用本教程前一部分中 `Query.getReadings` 操作的 `BatchGetItem` 字段配置来实施错误处理。这一次，我们假定在执行 `Query.getReadings` 字段时，`temperatureReadings` DynamoDB 表耗尽了预置的吞吐量。DynamoDB 在第二次尝试 Amazon AppSync 时引发了 **ProvisionedThroughputExceededException**a，用于处理批次中的剩余元素。

以下 JSON 表示在 DynamoDB 批处理调用之后但在评估响应映射模板之前的序列化上下文。

```
{
  "arguments": {
    "sensorId": "1",
    "timestamp": "2018-02-01T17:21:05.000+08:00"
  },
  "source": null,
  "result": {
    "data": {
      "temperatureReadings": [
        null
      ],
      "locationReadings": [
        {
          "lat": 47.615063,
          "long": -122.333551,
          "sensorId": "1",
          "timestamp": "2018-02-01T17:21:05.000+08:00"
        }
      ]
    },
    "unprocessedKeys": {
      "temperatureReadings": [
        {
          "sensorId": "1",
          "timestamp": "2018-02-01T17:21:05.000+08:00"
        }
      ],
      "locationReadings": []
    }
  },
  "error": {
    "type": "DynamoDB:ProvisionedThroughputExceededException",
    "message": "You exceeded your maximum allowed provisioned throughput for a table or for one or more global secondary indexes. (...)"
  },
  "outErrors": []
}
```

关于上下文需要注意的几点：
+ 已在`$ctx.error`上下文中设置了调用错误 Amazon AppSync，错误类型已设置为 Dynam **oDB**:。ProvisionedThroughputExceededException
+ 即使存在错误，也会在 `$ctx.result.data` 中为每个表映射结果
+ 在 `$ctx.result.data.unprocessedKeys` 中提供了未处理的键。在这里， Amazon AppSync 由于表吞吐量不足，无法使用密钥检索项目（sensorid:1，timestamp: 2018-02-01T 17:21:05.000 \$1 08:00）。

 **注意**：对于 `BatchPutItem`，它是 `$ctx.result.data.unprocessedItems`。对于 `BatchDeleteItem`，它是 `$ctx.result.data.unprocessedKeys`。

我们通过三种不同方式处理此错误。

#### 1. 承受调用错误


返回数据而不处理调用错误：这会有效地承受此错误，同时使给定 GraphQL 字段的结果始终成功。

我们编写的响应映射模板是熟悉的，仅侧重于结果数据。

响应映射模板：

```
$util.toJson($ctx.result.data)
```

GraphQL 响应：

```
{
  "data": {
    "getReadings": [
      {
        "sensorId": "1",
        "timestamp": "2018-02-01T17:21:05.000+08:00",
        "lat": 47.615063,
        "long": -122.333551
      },
      {
        "sensorId": "1",
        "timestamp": "2018-02-01T17:21:05.000+08:00",
        "value": 85.5
      }
    ]
  }
}
```

将不向错误响应中添加任何错误，因为只对数据执行了操作。

#### 2. 引发错误以中止模板执行


从客户端角度看，在应将部分失败视为完全失败时，您可以中止执行模板以防止返回数据。`$util.error(...)` 实用程序方法实现完全此行为。

响应映射模板：

```
## there was an error let's mark the entire field
## as failed and do not return any data back in the response
#if ($ctx.error)
    $util.error($ctx.error.message, $ctx.error.type, null, $ctx.result.data.unprocessedKeys)
#end

$util.toJson($ctx.result.data)
```

GraphQL 响应：

```
{
  "data": {
    "getReadings": null
  },
  "errors": [
    {
      "path": [
        "getReadings"
      ],
      "data": null,
      "errorType": "DynamoDB:ProvisionedThroughputExceededException",
      "errorInfo": {
        "temperatureReadings": [
          {
            "sensorId": "1",
            "timestamp": "2018-02-01T17:21:05.000+08:00"
          }
        ],
        "locationReadings": []
      },
      "locations": [
        {
          "line": 58,
          "column": 3
        }
      ],
      "message": "You exceeded your maximum allowed provisioned throughput for a table or for one or more global secondary indexes. (...)"
    }
  ]
}
```

即使可能已从 DynamoDB 批处理操作返回了一些结果，我们也选择引发错误，这样，`getReadings` GraphQL 字段为 Null，并且此错误已添加到 GraphQL 响应的*错误*数据块中。

#### 3. 追加错误以返回数据和错误


在某些情况下，为了提供更好的用户体验，应用程序可以返回部分结果并向其客户端通知未处理的项目。客户端可以决定是实施重试，还是将错误翻译出来并返回给最终用户。`$util.appendError(...)` 是一个可以启用此行为的实用程序方法，具体方式为：让应用程序设计人员在上下文中追加错误，而不干扰对模板的评估。评估模板后， Amazon AppSync 将通过将任何上下文错误附加到 GraphQL 响应的错误块来处理这些错误。

响应映射模板：

```
#if ($ctx.error)
    ## pass the unprocessed keys back to the caller via the `errorInfo` field
    $util.appendError($ctx.error.message, $ctx.error.type, null, $ctx.result.data.unprocessedKeys)
#end

$util.toJson($ctx.result.data)
```

我们在 GraphQL 响应的错误块中转发了调用错误和 unprocessedKeys 元素。`getReadings` 字段也从 **locationReadings** 表中返回部分数据，如下面的响应中所示。

GraphQL 响应：

```
{
  "data": {
    "getReadings": [
      null,
      {
        "sensorId": "1",
        "timestamp": "2018-02-01T17:21:05.000+08:00",
        "value": 85.5
      }
    ]
  },
  "errors": [
    {
      "path": [
        "getReadings"
      ],
      "data": null,
      "errorType": "DynamoDB:ProvisionedThroughputExceededException",
      "errorInfo": {
        "temperatureReadings": [
          {
            "sensorId": "1",
            "timestamp": "2018-02-01T17:21:05.000+08:00"
          }
        ],
        "locationReadings": []
      },
      "locations": [
        {
          "line": 58,
          "column": 3
        }
      ],
      "message": "You exceeded your maximum allowed provisioned throughput for a table or for one or more global secondary indexes. (...)"
    }
  ]
}
```

# 在中执行 DynamoDB 事务 Amazon AppSync
执行 DynamoDB 事务

**注意**  
我们现在主要支持 APPSYNC\$1JS 运行时系统及其文档。请考虑使用 APPSYNC\$1JS 运行时系统和[此处](https://docs.amazonaws.cn/appsync/latest/devguide/tutorials-js.html)的指南。

Amazon AppSync 支持对单个区域中的一个或多个表使用 Amazon DynamoDB 事务操作。支持的操作为 `TransactGetItems` 和 `TransactWriteItems`。通过在中使用这些功能 Amazon AppSync，您可以执行以下任务：
+ 在单个查询中传递键列表，并从表中返回结果
+ 在单个查询中从一个或多个表读取记录
+ 以 all-or-nothing某种方式将事务中的记录写入一个或多个表
+ 在满足某些条件时执行事务

## Permissions


与其他解析器一样，您需要在中创建数据源， Amazon AppSync 然后创建角色或使用现有角色。由于事务操作需要具有 DynamoDB 表的不同权限，因此，您需要为配置的角色授予读取或写入操作权限：

------
#### [ JSON ]

****  

```
{
    "Version":"2012-10-17",		 	 	 
    "Statement": [
        {
            "Action": [
                "dynamodb:DeleteItem",
                "dynamodb:GetItem",
                "dynamodb:PutItem",
                "dynamodb:Query",
                "dynamodb:Scan",
                "dynamodb:UpdateItem"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:dynamodb:us-east-1:111122223333:table/TABLENAME",
                "arn:aws:dynamodb:us-east-1:111122223333:table/TABLENAME/*"
            ]
        }
    ]
}
```

------

 **注意**：角色与中的数据源相关联 Amazon AppSync，字段上的解析器是针对数据源调用的。配置为针对 DynamoDB 获取的数据来源仅指定一个表，以使配置保持简单。因此，当在单个解析器中针对多个表执行事务操作（这是一项更高级的任务）时，您必须向该数据来源的角色授予对将与解析器进行交互的任何表的访问权限。这项操作将在上面 IAM 策略中的 **Resource (资源)** 字段中执行。针对表的事务调用配置在解析器模板中完成，下面将介绍相关内容。

## 数据来源


为简便起见，我们将为本教程中使用的所有解析器使用同一个数据来源。在**数据源**选项卡上，创建一个新的 DynamoDB 数据源并将其命名。**TransactTutorial**表名称可以是任何内容，因为表名称被指定为事务操作的请求映射模板的一部分。我们将提供表名称 `empty`。

我们将有两个表，分别名为 **savingAccounts** 和 **checkingAccounts**，两个表都使用 `accountNumber` 作为分区键，还有一个 **transactionHistory** 表，该表使用 `transactionId` 作为分区键。

对于本教程，具有以下内联策略的任何角色都有效。将 `region` 和 `accountId` 替换为您的区域和账户 ID：

------
#### [ JSON ]

****  

```
{
    "Version":"2012-10-17",		 	 	 
    "Statement": [
        {
            "Action": [
                "dynamodb:DeleteItem",
                "dynamodb:GetItem",
                "dynamodb:PutItem",
                "dynamodb:Query",
                "dynamodb:Scan",
                "dynamodb:UpdateItem"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:dynamodb:us-east-1:111122223333:table/savingAccounts",
                "arn:aws:dynamodb:us-east-1:111122223333:table/savingAccounts/*",
                "arn:aws:dynamodb:us-east-1:111122223333:table/checkingAccounts",
                "arn:aws:dynamodb:us-east-1:111122223333:table/checkingAccounts/*",
                "arn:aws:dynamodb:us-east-1:111122223333:table/transactionHistory",
                "arn:aws:dynamodb:us-east-1:111122223333:table/transactionHistory/*"
            ]
        }
    ]
}
```

------

## 事务


对于本示例，上下文是一个典型的银行交易，我们将使用 `TransactWriteItems` 来执行以下操作：
+ 从储蓄账户转账到支票账户
+ 为每个交易生成新的交易记录

然后我们将使用 `TransactGetItems` 从储蓄账户和支票账户中检索详细信息。

**警告**  
与冲突检测和解决功能一起使用时不支持 `TransactWriteItems`。必须禁用这些设置以防止可能出现的错误。

我们按如下所示定义我们的 GraphQL 架构：

```
type SavingAccount {
    accountNumber: String!
    username: String
    balance: Float
}

type CheckingAccount {
    accountNumber: String!
    username: String
    balance: Float
}

type TransactionHistory {
    transactionId: ID!
    from: String
    to: String
    amount: Float
}

type TransactionResult {
    savingAccounts: [SavingAccount]
    checkingAccounts: [CheckingAccount]
    transactionHistory: [TransactionHistory]
}

input SavingAccountInput {
    accountNumber: String!
    username: String
    balance: Float
}

input CheckingAccountInput {
    accountNumber: String!
    username: String
    balance: Float
}

input TransactionInput {
    savingAccountNumber: String!
    checkingAccountNumber: String!
    amount: Float!
}

type Query {
    getAccounts(savingAccountNumbers: [String], checkingAccountNumbers: [String]): TransactionResult
}

type Mutation {
    populateAccounts(savingAccounts: [SavingAccountInput], checkingAccounts: [CheckingAccountInput]): TransactionResult
    transferMoney(transactions: [TransactionInput]): TransactionResult
}

schema {
    query: Query
    mutation: Mutation
}
```

### TransactWriteItems -填充账户


为了在账户之间转账，我们需要用详细信息填充表格。我们将使用 GraphQL 操作 `Mutation.populateAccounts` 来实现此目的。

在“架构”部分中，单击 `Mutation.populateAccounts` 操作旁边的**附加**。转到“VTL 单位解析器”，然后选择相同的 `TransactTutorial` 数据来源。

现在使用以下请求映射模板：

 **请求映射模板** 

```
#set($savingAccountTransactPutItems = [])
#set($index = 0)
#foreach($savingAccount in ${ctx.args.savingAccounts})
    #set($keyMap = {})
    $util.qr($keyMap.put("accountNumber", $util.dynamodb.toString($savingAccount.accountNumber)))
    #set($attributeValues = {})
    $util.qr($attributeValues.put("username", $util.dynamodb.toString($savingAccount.username)))
    $util.qr($attributeValues.put("balance", $util.dynamodb.toNumber($savingAccount.balance)))
    #set($index = $index + 1)
    #set($savingAccountTransactPutItem = {"table": "savingAccounts",
        "operation": "PutItem",
        "key": $keyMap,
        "attributeValues": $attributeValues})
    $util.qr($savingAccountTransactPutItems.add($savingAccountTransactPutItem))
#end

#set($checkingAccountTransactPutItems = [])
#set($index = 0)
#foreach($checkingAccount in ${ctx.args.checkingAccounts})
    #set($keyMap = {})
    $util.qr($keyMap.put("accountNumber", $util.dynamodb.toString($checkingAccount.accountNumber)))
    #set($attributeValues = {})
    $util.qr($attributeValues.put("username", $util.dynamodb.toString($checkingAccount.username)))
    $util.qr($attributeValues.put("balance", $util.dynamodb.toNumber($checkingAccount.balance)))
    #set($index = $index + 1)
    #set($checkingAccountTransactPutItem = {"table": "checkingAccounts",
        "operation": "PutItem",
        "key": $keyMap,
        "attributeValues": $attributeValues})
    $util.qr($checkingAccountTransactPutItems.add($checkingAccountTransactPutItem))
#end

#set($transactItems = [])
$util.qr($transactItems.addAll($savingAccountTransactPutItems))
$util.qr($transactItems.addAll($checkingAccountTransactPutItems))

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

和以下响应映射模板：

 **响应映射模板** 

```
#if ($ctx.error)
    $util.appendError($ctx.error.message, $ctx.error.type, null, $ctx.result.cancellationReasons)
#end

#set($savingAccounts = [])
#foreach($index in [0..2])
    $util.qr($savingAccounts.add(${ctx.result.keys[$index]}))
#end

#set($checkingAccounts = [])
#foreach($index in [3..5])
    $util.qr($checkingAccounts.add(${ctx.result.keys[$index]}))
#end

#set($transactionResult = {})
$util.qr($transactionResult.put('savingAccounts', $savingAccounts))
$util.qr($transactionResult.put('checkingAccounts', $checkingAccounts))

$util.toJson($transactionResult)
```

保存解析器并导航到 Amazon AppSync 控制台的 “**查询**” 部分以填充账户。

执行以下变更：

```
mutation populateAccounts {
  populateAccounts (
    savingAccounts: [
      {accountNumber: "1", username: "Tom", balance: 100},
      {accountNumber: "2", username: "Amy", balance: 90},
      {accountNumber: "3", username: "Lily", balance: 80},
    ]
    checkingAccounts: [
      {accountNumber: "1", username: "Tom", balance: 70},
      {accountNumber: "2", username: "Amy", balance: 60},
      {accountNumber: "3", username: "Lily", balance: 50},
    ]) {
    savingAccounts {
      accountNumber
    }
    checkingAccounts {
      accountNumber
    }
  }
}
```

我们在一个变更中填充了 3 个储蓄账户和 3 个支票账户。

使用 DynamoDB 控制台验证是否在 **savingAccounts** 和 **checkingAccounts** 表中显示数据。

### TransactWriteItems -转账


使用以下**请求映射模板**将一个解析器附加到 `transferMoney` 变更。请注意 `amounts`、`savingAccountNumbers` 和 `checkingAccountNumbers` 的值相同。

```
#set($amounts = [])
#foreach($transaction in ${ctx.args.transactions})
    #set($attributeValueMap = {})
    $util.qr($attributeValueMap.put(":amount", $util.dynamodb.toNumber($transaction.amount)))
    $util.qr($amounts.add($attributeValueMap))
#end

#set($savingAccountTransactUpdateItems = [])
#set($index = 0)
#foreach($transaction in ${ctx.args.transactions})
    #set($keyMap = {})
    $util.qr($keyMap.put("accountNumber", $util.dynamodb.toString($transaction.savingAccountNumber)))
    #set($update = {})
    $util.qr($update.put("expression", "SET balance = balance - :amount"))
    $util.qr($update.put("expressionValues", $amounts[$index]))
    #set($index = $index + 1)
    #set($savingAccountTransactUpdateItem = {"table": "savingAccounts",
        "operation": "UpdateItem",
        "key": $keyMap,
        "update": $update})
    $util.qr($savingAccountTransactUpdateItems.add($savingAccountTransactUpdateItem))
#end

#set($checkingAccountTransactUpdateItems = [])
#set($index = 0)
#foreach($transaction in ${ctx.args.transactions})
    #set($keyMap = {})
    $util.qr($keyMap.put("accountNumber", $util.dynamodb.toString($transaction.checkingAccountNumber)))
    #set($update = {})
    $util.qr($update.put("expression", "SET balance = balance + :amount"))
    $util.qr($update.put("expressionValues", $amounts[$index]))
    #set($index = $index + 1)
    #set($checkingAccountTransactUpdateItem = {"table": "checkingAccounts",
        "operation": "UpdateItem",
        "key": $keyMap,
        "update": $update})
    $util.qr($checkingAccountTransactUpdateItems.add($checkingAccountTransactUpdateItem))
#end

#set($transactionHistoryTransactPutItems = [])
#foreach($transaction in ${ctx.args.transactions})
    #set($keyMap = {})
    $util.qr($keyMap.put("transactionId", $util.dynamodb.toString(${utils.autoId()})))
    #set($attributeValues = {})
    $util.qr($attributeValues.put("from", $util.dynamodb.toString($transaction.savingAccountNumber)))
    $util.qr($attributeValues.put("to", $util.dynamodb.toString($transaction.checkingAccountNumber)))
    $util.qr($attributeValues.put("amount", $util.dynamodb.toNumber($transaction.amount)))
    #set($transactionHistoryTransactPutItem = {"table": "transactionHistory",
        "operation": "PutItem",
        "key": $keyMap,
        "attributeValues": $attributeValues})
    $util.qr($transactionHistoryTransactPutItems.add($transactionHistoryTransactPutItem))
#end

#set($transactItems = [])
$util.qr($transactItems.addAll($savingAccountTransactUpdateItems))
$util.qr($transactItems.addAll($checkingAccountTransactUpdateItems))
$util.qr($transactItems.addAll($transactionHistoryTransactPutItems))

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

我们将在一个 `TransactWriteItems` 操作中进行 3 笔银行交易。使用以下**响应映射模板**：

```
#if ($ctx.error)
    $util.appendError($ctx.error.message, $ctx.error.type, null, $ctx.result.cancellationReasons)
#end

#set($savingAccounts = [])
#foreach($index in [0..2])
    $util.qr($savingAccounts.add(${ctx.result.keys[$index]}))
#end

#set($checkingAccounts = [])
#foreach($index in [3..5])
    $util.qr($checkingAccounts.add(${ctx.result.keys[$index]}))
#end

#set($transactionHistory = [])
#foreach($index in [6..8])
    $util.qr($transactionHistory.add(${ctx.result.keys[$index]}))
#end

#set($transactionResult = {})
$util.qr($transactionResult.put('savingAccounts', $savingAccounts))
$util.qr($transactionResult.put('checkingAccounts', $checkingAccounts))
$util.qr($transactionResult.put('transactionHistory', $transactionHistory))

$util.toJson($transactionResult)
```

现在导航到 Amazon AppSync 控制台的 “**查询**” 部分，按如下方式执行 **TransferMoney** 变更：

```
mutation write {
  transferMoney(
    transactions: [
      {savingAccountNumber: "1", checkingAccountNumber: "1", amount: 7.5},
      {savingAccountNumber: "2", checkingAccountNumber: "2", amount: 6.0},
      {savingAccountNumber: "3", checkingAccountNumber: "3", amount: 3.3}
    ]) {
    savingAccounts {
      accountNumber
    }
    checkingAccounts {
      accountNumber
    }
    transactionHistory {
      transactionId
    }
  }
}
```

我们在一个变更中发送了 2 笔银行交易。使用 DynamoDB 控制台验证是否在 **savingAccounts**、**checkingAccounts** 和 **transactionHistory** 表中显示数据。

### TransactGetItems -找回账户


为了在单个交易请求中从储蓄账户和支票账户检索详细信息，我们需要将解析器附加到架构上的 `Query.getAccounts` GraphQL 操作。选择**附加**，转到“VTL 单位解析器”，然后在下一个屏幕上选择在教程开始时创建的相同 `TransactTutorial` 数据来源。按如下所示配置模板：

 **请求映射模板** 

```
#set($savingAccountsTransactGets = [])
#foreach($savingAccountNumber in ${ctx.args.savingAccountNumbers})
    #set($savingAccountKey = {})
    $util.qr($savingAccountKey.put("accountNumber", $util.dynamodb.toString($savingAccountNumber)))
    #set($savingAccountTransactGet = {"table": "savingAccounts", "key": $savingAccountKey})
    $util.qr($savingAccountsTransactGets.add($savingAccountTransactGet))
#end

#set($checkingAccountsTransactGets = [])
#foreach($checkingAccountNumber in ${ctx.args.checkingAccountNumbers})
    #set($checkingAccountKey = {})
    $util.qr($checkingAccountKey.put("accountNumber", $util.dynamodb.toString($checkingAccountNumber)))
    #set($checkingAccountTransactGet = {"table": "checkingAccounts", "key": $checkingAccountKey})
    $util.qr($checkingAccountsTransactGets.add($checkingAccountTransactGet))
#end

#set($transactItems = [])
$util.qr($transactItems.addAll($savingAccountsTransactGets))
$util.qr($transactItems.addAll($checkingAccountsTransactGets))

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

 **响应映射模板** 

```
#if ($ctx.error)
    $util.appendError($ctx.error.message, $ctx.error.type, null, $ctx.result.cancellationReasons)
#end

#set($savingAccounts = [])
#foreach($index in [0..2])
    $util.qr($savingAccounts.add(${ctx.result.items[$index]}))
#end

#set($checkingAccounts = [])
#foreach($index in [3..4])
    $util.qr($checkingAccounts.add($ctx.result.items[$index]))
#end

#set($transactionResult = {})
$util.qr($transactionResult.put('savingAccounts', $savingAccounts))
$util.qr($transactionResult.put('checkingAccounts', $checkingAccounts))

$util.toJson($transactionResult)
```

保存解析器并导航到 Amazon AppSync 控制台的 “**查询**” 部分。为了检索储蓄账户和支票账户，请执行以下查询：

```
query getAccounts {
  getAccounts(
    savingAccountNumbers: ["1", "2", "3"],
    checkingAccountNumbers: ["1", "2"]
  ) {
    savingAccounts {
      accountNumber
      username
      balance
    }
    checkingAccounts {
      accountNumber
      username
      balance
    }
  }
}
```

我们已经成功演示了使用的 DynamoDB 事务的用法。 Amazon AppSync

# 在中使用 HTTP 解析器 Amazon AppSync
使用 HTTP 解析器

**注意**  
我们现在主要支持 APPSYNC\$1JS 运行时系统及其文档。请考虑使用 APPSYNC\$1JS 运行时系统和[此处](https://docs.amazonaws.cn/appsync/latest/devguide/tutorials-js.html)的指南。

Amazon AppSync 允许您使用支持的数据源（即亚马逊 DynamoDB Amazon Lambda、 OpenSearch 亚马逊服务或 Amazon Aurora）执行各种操作，此外还可以使用任何任意 HTTP 终端节点来解析 GraphQL 字段。在您的 HTTP 终端节点可用后，您可以使用数据来源连接它们。然后，您可以在架构中配置一个解析器以执行 GraphQL 操作（如查询、变更和订阅）。本教程将引导您了解一些常见示例。

在本教程中，您将使用 REST API（使用 Amazon API Gateway 和 Lambda 创建）和 GraphQL Amazon AppSync 终端节点。

## 一键设置


如果您想在 Amazon AppSync 配置了 HTTP 终端节点的情况下自动设置 GraphQL 终端节点（使用 Amazon API Gateway 和 Lambda），则可以使用以下模板： Amazon CloudFormation 

[https://console.amazonaws.cn/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3.us-west-2.amazonaws.com/awsappsync/resources/http/http-full.yaml](https://console.amazonaws.cn/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3.us-west-2.amazonaws.com/awsappsync/resources/http/http-full.yaml)

## 创建 REST API


您可以使用以下 Amazon CloudFormation 模板来设置适用于本教程的 REST 端点：

[https://console.amazonaws.cn/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3.us-west-2.amazonaws.com/awsappsync/resources/http/http-api-gw.yaml](https://console.amazonaws.cn/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3.us-west-2.amazonaws.com/awsappsync/resources/http/http-api-gw.yaml)

 Amazon CloudFormation 堆栈执行以下步骤：

1. 设置 Lambda 函数，其中包含您的微服务的业务逻辑。

1. 使用以下endpoint/method/content类型组合设置 API Gateway REST API：


****  

| API 资源路径 | HTTP 方法 | 支持的内容类型 | 
| --- | --- | --- | 
|  /v1/users  |  POST  |  application/json  | 
|  /v1/users  |  GET  |  application/json  | 
|  /v1/users/1  |  GET  |  application/json  | 
|  /v1/users/1  |  PUT  |  application/json  | 
|  /v1/users/1  |  DELETE  |  application/json  | 

## 创建您的 GraphQL API


要在以下位置创建 GraphQL API，请执行以下操作： Amazon AppSync
+ 打开 Amazon AppSync 控制台并选择**创建 API**。
+ 对于 API 名称，请键入 `UserData`。
+ 选择**自定义架构**。
+ 选择**创建**。

 Amazon AppSync 控制台使用 API 密钥身份验证模式为您创建一个新的 GraphQL API。您可以根据本教程后面的说明，使用控制台设置 GraphQL API 的其余部分，并针对它运行查询。

## 创建 GraphQL 架构


现在，您有一个 GraphQL API，让我们创建一个 GraphQL 架构。在 Amazon AppSync 控制台的架构编辑器中，确保架构与以下架构相匹配：

```
schema {
    query: Query
    mutation: Mutation
}

type Mutation {
    addUser(userInput: UserInput!): User
    deleteUser(id: ID!): User
}

type Query {
    getUser(id: ID): User
    listUser: [User!]!
}

type User {
    id: ID!
    username: String!
    firstname: String
    lastname: String
    phone: String
    email: String
}

input UserInput {
    id: ID!
    username: String!
    firstname: String
    lastname: String
    phone: String
    email: String
}
```

## 配置您的 HTTP 数据来源


要配置 HTTP 数据来源，请执行以下操作：
+ 在**DataSources**选项卡上，选择 “**新建**”，然后键入数据源的友好名称（例如`HTTP`）。
+ 在 **Data source type (数据来源类型)** 中，选择 **HTTP**。
+ 将终端节点设置为创建的 API 网关终端节点。确保不将阶段名称作为终端节点的一部分包含在内。

 **注意：**目前 Amazon AppSync 仅支持公共终端节点。

 **注意：**有关该 Amazon AppSync 服务识别的认证机构的更多信息，请参阅 [HTTPS 端点的证书颁发机构 (CA) 认可 Amazon AppSync](http-cert-authorities.md#aws-appsync-http-certificate-authorities)。

## 配置解析器


在此步骤中，您将 HTTP 数据来源连接到 **getUser** 查询。

设置解析器：
+ 选择**架构**选项卡。
+ 在右侧的**数据类型**窗格中，在 **Query** 类型下面找到 **getUser** 字段，然后选择**附加**。
+ 在 **Data source name (数据来源名称)** 中，选择 **HTTP**。
+ 在 **Configure the request mapping template (配置请求映射模板)** 中，粘贴以下代码：

```
{
    "version": "2018-05-29",
    "method": "GET",
    "params": {
        "headers": {
            "Content-Type": "application/json"
        }
    },
    "resourcePath": $util.toJson("/v1/users/${ctx.args.id}")
}
```
+ 在 **Configure the response mapping template (配置响应映射模板)** 中，粘贴以下代码：

```
## return the body
#if($ctx.result.statusCode == 200)
    ##if response is 200
    $ctx.result.body
#else
    ##if response is not 200, append the response to error block.
    $utils.appendError($ctx.result.body, "$ctx.result.statusCode")
#end
```
+ 选择 **Query (查询)** 选项卡，然后运行以下查询：

```
query GetUser{
    getUser(id:1){
        id
        username
    }
}
```

此查询应返回以下响应：

```
{
    "data": {
        "getUser": {
            "id": "1",
            "username": "nadia"
        }
    }
}
```
+ 选择**架构**选项卡。
+ 在右侧的**数据类型**窗格中，在 **Mutation** 下面找到 **addUser** 字段，然后选择**附加**。
+ 在 **Data source name (数据来源名称)** 中，选择 **HTTP**。
+ 在 **Configure the request mapping template (配置请求映射模板)** 中，粘贴以下代码：

```
{
    "version": "2018-05-29",
    "method": "POST",
    "resourcePath": "/v1/users",
    "params":{
      "headers":{
        "Content-Type": "application/json",
      },
      "body": $util.toJson($ctx.args.userInput)
    }
}
```
+ 在 **Configure the response mapping template (配置响应映射模板)** 中，粘贴以下代码：

```
## Raise a GraphQL field error in case of a datasource invocation error
#if($ctx.error)
    $util.error($ctx.error.message, $ctx.error.type)
#end
## if the response status code is not 200, then return an error. Else return the body **
#if($ctx.result.statusCode == 200)
    ## If response is 200, return the body.
    $ctx.result.body
#else
    ## If response is not 200, append the response to error block.
    $utils.appendError($ctx.result.body, "$ctx.result.statusCode")
#end
```
+ 选择 **Query (查询)** 选项卡，然后运行以下查询：

```
mutation addUser{
    addUser(userInput:{
        id:"2",
        username:"shaggy"
    }){
        id
        username
    }
}
```

此查询应返回以下响应：

```
{
    "data": {
        "getUser": {
        "id": "2",
        "username": "shaggy"
        }
    }
}
```

## 调用服务 Amazon


您可以使用 HTTP 解析器为服务设置 GraphQL API 接口 Amazon 。对的 HTTP 请求 Amazon 必须使用[签名版本 4 流程](https://docs.amazonaws.cn/general/latest/gr/signature-version-4.html)进行签名，这样 Amazon 才能识别谁发送了这些请求。 Amazon AppSync 当您将 IAM 角色与 HTTP 数据源关联时，代表您计算签名。

您还提供了两个额外的组件，用于通过 HTTP 解析器调用 Amazon 服务：
+ 有权调用 Amazon 服务的 IAM 角色 APIs
+ 数据来源中的签名配置

例如，如果您想使用 HTTP 解析器调用该[ListGraphqlApis 操作](https://docs.amazonaws.cn/appsync/latest/APIReference/API_ListGraphqlApis.html)，请先[创建一个附加以下策略的 IAM 角色](attaching-a-data-source.md#aws-appsync-getting-started-build-a-schema-from-scratch)： Amazon AppSync 

------
#### [ JSON ]

****  

```
{
    "Version":"2012-10-17",		 	 	 
    "Statement": [
        {
            "Action": [
                "appsync:ListGraphqlApis"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}
```

------

接下来，为创建 HTTP 数据源 Amazon AppSync。在本示例中，您在美国西部（俄勒冈）区域调用 Amazon AppSync 。在名为 `http.json` 文件中设置以下 HTTP 配置，其中包括签名区域和服务名称：

```
{
    "endpoint": "https://appsync.us-west-2.amazonaws.com/",
    "authorizationConfig": {
        "authorizationType": "AWS_IAM",
        "awsIamConfig": {
            "signingRegion": "us-west-2",
            "signingServiceName": "appsync"
        }
    }
}
```

然后，使用创建具有关联角色的数据源，如下所示： Amazon CLI 

```
aws appsync create-data-source --api-id <API-ID> \
                               --name AWSAppSync \
                               --type HTTP \
                               --http-config file:///http.json \
                               --service-role-arn <ROLE-ARN>
```

当您将解析器附加到架构中的字段时，请使用以下请求映射模板进行调用 Amazon AppSync：

```
{
    "version": "2018-05-29",
    "method": "GET",
    "resourcePath": "/v1/apis"
}
```

当您对该数据源运行 GraphQL 查询时，使用您提供的角色对请求进行 Amazon AppSync 签名，并在请求中包含签名。该查询会返回您在该区域的账户 APIs 中的 Amazon AppSync GraphQL 列表。 Amazon 

# 将 Aurora Serverless v2 与 Amazon AppSync
使用 Aurora Serverless v2 解析器

使用将您的 GraphQL API 连接到 Aurora 无服务器数据库。 Amazon AppSync借助这种集成，您可以通过 GraphQL 查询、变更和订阅来执行 SQL 语句，从而为您提供与关系数据交互的灵活方式。

**注意**  
本教程使用 `US-EAST-1` 区域。

**优势**
+ GraphQL 和关系数据库之间的无缝集成
+ 能够通过 GraphQL 接口执行 SQL 操作
+ Aurora Serverless v2 的无服务器可扩展性
+ 通过 Secr Amazon ets Manager 安全访问数据
+ 通过输入清理来防止 SQL 注入
+ 灵活的查询功能，包括筛选和范围操作

**常见使用案例**
+ 根据关系数据要求构建可扩展的应用程序
+  APIs 既需要 GraphQL 灵活性又需要 SQL 数据库功能的创作
+ 通过 GraphQL 变更和查询来管理数据操作
+ 实施安全的数据库访问模式

在本教程中，您将学习以下内容。
+ 设置 Aurora Serverless v2 集群
+ 启用数据 API 功能
+ 创建和配置数据库结构
+ 为数据库操作定义 GraphQL 架构
+ 实施查询和变更的解析器
+ 通过适当的输入清理来保护您的数据访问
+ 通过 GraphQL 接口执行各种数据库操作

**Topics**
+ [

## 设置数据库集群
](#create-cluster)
+ [

## 启用数据 API
](#enable-data-api)
+ [

## 创建数据库和表
](#create-database-and-table)
+ [

## GraphQL 架构
](#graphql-schema)
+ [

## 将您的 API 连接到数据库操作
](#configuring-resolvers)
+ [

## 通过 API 修改数据
](#run-mutations)
+ [

## 检索数据
](#run-queries)
+ [

## 保护您的数据访问
](#input-sanitization)

## 设置数据库集群


在向添加 Amazon RDS 数据源之前 Amazon AppSync，必须先在 Aurora Serverless v2 集群上启用数据 API，然后**使用配置密钥**。*Amazon Secrets Manager*您可以使用 Amazon CLI创建 Aurora Serverless v2 集群：

```
aws rds create-db-cluster \
    --db-cluster-identifier appsync-tutorial \
    --engine aurora-mysql \
    --engine-version 8.0 \
    --serverless-v2-scaling-configuration MinCapacity=0,MaxCapacity=1 \
    --master-username USERNAME \
    --master-user-password COMPLEX_PASSWORD \
    --enable-http-endpoint
```

这将为集群返回一个 ARN。

创建集群后，必须使用以下命令添加一个 Aurora Serverless v2 实例。

```
aws rds create-db-instance \
    --db-cluster-identifier appsync-tutorial \
    --db-instance-identifier appsync-tutorial-instance-1 \
    --db-instance-class db.serverless \
    --engine aurora-mysql
```

**注意**  
这些端点需要一段时间才会活跃。您可以在 Amazon RDS 控制台中相应集群的**连接和安全**选项卡中查看其状态。您也可以使用以下 Amazon CLI 命令检查集群的状态。  

```
aws rds describe-db-clusters \
    --db-cluster-identifier appsync-tutorial \
    --query "DBClusters[0].Status"
```

您可以使用 Amazon Secrets Manager 控制台创建 S *ecre* t，也可以使用上 Amazon CLI 一步中的`USERNAME`和`COMPLEX_PASSWORD`使用输入文件创建 Secret，如下所示。

```
{
    "username": "USERNAME",
    "password": "COMPLEX_PASSWORD"
}
```

将其作为参数传递给 Amazon CLI：

```
aws secretsmanager create-secret --name HttpRDSSecret --secret-string file://creds.json --region us-east-1
```

这将为密钥返回 ARN。

 **记下您的 Aurora Serverless 集群的 ARN** 和密钥，以便以后在创建数据源时在 AppSync 控制台中使用。

## 启用数据 API


您可以通过[按照 RDS 文档中的说明操作](https://docs.amazonaws.cn/AmazonRDS/latest/AuroraUserGuide/data-api.html)来在您的集群上启用数据 API。在添加为数据源之前，必须启用 AppSync 数据 API。

## 创建数据库和表


在启用数据 API 后，您可以确保它在 Amazon CLI中使用 `aws rds-data execute-statement` 命令。这将确保您的 Aurora 无服务器集群在添加到 AppSync API 之前已正确配置该集群。首先，使用 `--sql` 参数创建一个名为 *TESTDB* 的数据库，如下所示：

```
aws rds-data execute-statement --resource-arn "arn:aws:rds:us-east-1:123456789000:cluster:http-endpoint-test" \
--schema "mysql"  --secret-arn "arn:aws:secretsmanager:us-east-1:123456789000:secret:testHttp2-AmNvc1"  \
--region us-east-1 --sql "create DATABASE TESTDB"
```

如果运行无误，请使用*创建表* 命令添加一个表：

```
aws rds-data execute-statement --resource-arn "arn:aws:rds:us-east-1:123456789000:cluster:http-endpoint-test" \
 --schema "mysql"  --secret-arn "arn:aws:secretsmanager:us-east-1:123456789000:secret:testHttp2-AmNvc1" \
 --region us-east-1 \
 --sql "create table Pets(id varchar(200), type varchar(200), price float)" --database "TESTDB"
```

如果一切运行顺利，则可以继续在 AppSync API 中添加集群作为数据源。

## GraphQL 架构


现在，您的 Aurora Serverless 数据 API 已通过表启动并运行，我们将创建一个 GraphQL 架构并附加用于执行变更和订阅的解析器。在 Amazon AppSync 控制台中创建新 API 并导航到 **Schema** 页面，然后输入以下内容：

```
type Mutation {
    createPet(input: CreatePetInput!): Pet
    updatePet(input: UpdatePetInput!): Pet
    deletePet(input: DeletePetInput!): Pet
}

input CreatePetInput {
    type: PetType
    price: Float!
}

input UpdatePetInput {
id: ID!
    type: PetType
    price: Float!
}

input DeletePetInput {
    id: ID!
}

type Pet {
    id: ID!
    type: PetType
    price: Float
}

enum PetType {
    dog
    cat
    fish
    bird
    gecko
}

type Query {
    getPet(id: ID!): Pet
    listPets: [Pet]
    listPetsByPriceRange(min: Float, max: Float): [Pet]
}

schema {
    query: Query
    mutation: Mutation
}
```

 **保存**您的架构并导航到**数据来源**页面，然后创建新的数据来源。为数据来源类型选择**关系数据库**，并提供一个友好名称。使用您在上一步中创建的数据库名称以及用来创建它的**集群 ARN**。对于该**角色**，您可以 AppSync 创建一个新角色，也可以使用类似于以下内容的策略创建一个角色：

------
#### [ JSON ]

****  

```
{
    "Version":"2012-10-17",		 	 	 
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "rds-data:BatchExecuteStatement",
                "rds-data:BeginTransaction",
                "rds-data:CommitTransaction",
                "rds-data:ExecuteStatement",
                "rds-data:RollbackTransaction"
            ],
            "Resource": [
                "arn:aws:rds:us-east-1:111122223333:cluster:mydbcluster",
                "arn:aws:rds:us-east-1:111122223333:cluster:mydbcluster:*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "secretsmanager:GetSecretValue"
            ],
            "Resource": [
            "arn:aws:secretsmanager:us-east-1:111122223333:secret:mysecret",
            "arn:aws:secretsmanager:us-east-1:111122223333:secret:mysecret:*"
            ]
        }
    ]
}
```

------

请注意，此策略中有两个需要获得角色访问权的**语句**。第一个**资源**是您的 Aurora 无服务器集群，第二个资源是您的 AR Amazon Secrets Manager N。在单击 “**创建**” 之前，您需要 ARNs 在 AppSync 数据源配置中**同时提供两**者。

将其作为参数传递给 Amazon CLI。

```
aws secretsmanager create-secret \
  --name HttpRDSSecret \
  --secret-string file://creds.json \
  --region us-east-1
```

这将为密钥返回 ARN。在控制台中创建数据源时，请记下您的 Aurora Serverless 集群的 ARN 和密钥，以备日后使用。 Amazon AppSync 

### 构建您的数据库结构


在启用数据 API 后，您可以确保它在 Amazon CLI中使用 `aws rds-data execute-statement` 命令。这将确保您的 Aurora Serverless v2 集群在添加到 API 之前已正确配置。 Amazon AppSync 首先，使用 `--sql` 参数创建名为 *TESTDB* 的数据库，如下所示。

```
aws rds-data execute-statement \
                --resource-arn "arn:aws:rds:us-east-1:111122223333:cluster:appsync-tutorial" \
                --secret-arn "arn:aws:secretsmanager:us-east-1:111122223333:secret:appsync-tutorial-rds-secret"  \
                --region us-east-1 \
                --sql "create DATABASE TESTDB"
```

如果运行无误，请使用以下 *create table* 命令添加一个表。

```
aws rds-data execute-statement \
      --resource-arn "arn:aws:rds:us-east-1:111122223333:cluster:http-endpoint-test" \
      --secret-arn "arn:aws:secretsmanager:us-east-1:111122223333:secret:testHttp2-AmNvc1" \
      --region us-east-1 \
      --sql "create table Pets(id varchar(200), type varchar(200), price float)" \
      --database "TESTDB"
```

### 设计您的 API 接口


Aurora Serverless v2 数据 API 已通过表启动并运行之后，创建一个 GraphQL 架构并附加用于执行变更和订阅的解析器。在 Amazon AppSync 控制台中创建新 API，然后在控制台中导航到**架构**页面，然后输入以下内容。

```
type Mutation {
        createPet(input: CreatePetInput!): Pet
        updatePet(input: UpdatePetInput!): Pet
        deletePet(input: DeletePetInput!): Pet
    }
    
    input CreatePetInput {
        type: PetType
        price: Float!
    }
    
    input UpdatePetInput {
        id: ID!
        type: PetType
        price: Float!
    }
    
    input DeletePetInput {
        id: ID!
    }
    
    type Pet {
        id: ID!
        type: PetType
        price: Float
    }
    
    enum PetType {
        dog
        cat
        fish
        bird
        gecko
    }
    
    type Query {
        getPet(id: ID!): Pet
        listPets: [Pet]
        listPetsByPriceRange(min: Float, max: Float): [Pet]
    }
    
    schema {
        query: Query
        mutation: Mutation
    }
```

 **保存**您的架构并导航到**数据来源**页面，然后创建新的数据来源。为**数据来源**类型选择**关系数据库**，并提供一个易于理解和记忆的名称。使用您在上一步中创建的数据库名称以及用来创建它的**集群 ARN**。对于该**角色**，您可以 Amazon AppSync 创建一个新角色，也可以使用类似于以下内容的策略创建一个角色。

------
#### [ JSON ]

****  

```
{
        "Version":"2012-10-17",		 	 	 
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "rds-data:BatchExecuteStatement",
                    "rds-data:BeginTransaction",
                    "rds-data:CommitTransaction",
                    "rds-data:ExecuteStatement",
                    "rds-data:RollbackTransaction"
                ],
                "Resource": [
                    "arn:aws:rds:us-east-1:111122223333:cluster:mydbcluster",
                    "arn:aws:rds:us-east-1:111122223333:cluster:mydbcluster:*"
                ]
            },
            {
                "Effect": "Allow",
                "Action": [
                    "secretsmanager:GetSecretValue"
                ],
                "Resource": [
                "arn:aws:secretsmanager:us-east-1:111122223333:secret:mysecret",
                "arn:aws:secretsmanager:us-east-1:111122223333:secret:mysecret:*"
                ]
            }
        ]
    }
```

------

请注意，此策略中有两个需要获得角色访问权的**语句**。第一个**资源**是你的 Aurora Serverless v2 集群，第二个资源是你的 AR Amazon Secrets Manager N。在单击 “**创建**” 之前，您需要 ARNs 在 Amazon AppSync 数据源配置中**同时提供两**者。

## 将您的 API 连接到数据库操作


现在，我们有一个有效的 GraphQL 架构和一个 RDS 数据来源，您可以将解析器附加到架构中的 GraphQL 字段。我们的 API 将提供以下功能：

1. 使用 *Mutation.createPet* 字段创建宠物

1. 使用 *Mutation.updatePet* 字段更新宠物

1. 使用 *Mutation.deletePet* 字段删除宠物

1. 使用 *Query.getPet* 字段获取单个宠物

1. 使用 *Query.listPets* 字段列出所有宠物

1. 使用*查询列出价格区间内的宠物。 listPetsByPriceRange*字段

### Mutation.createPet


在 Amazon AppSync 控制台的架构编辑器中，在右侧选择 Att **ach Resolver fo** `createPet(input: CreatePetInput!): Pet` r。选择您的 RDS 数据来源。在**请求映射模板**部分，添加以下模板：

```
#set($id=$utils.autoId())
{
"version": "2018-05-29",
    "statements": [
        "insert into Pets VALUES (:ID, :TYPE, :PRICE)",
        "select * from Pets WHERE id = :ID"
    ],
    "variableMap": {
        ":ID": "$ctx.args.input.id",
        ":TYPE": $util.toJson($ctx.args.input.type),
        ":PRICE": $util.toJson($ctx.args.input.price)
    }
}
```

系统会根据**语句**数组中的顺序依次执行 SQL 语句。结果将以相同的顺序返回。由于这是一个变更，您将在 *insert* 后面运行 *select* 语句来检索提交的值，从而填充 GraphQL 响应映射模板。

在**响应映射模板**部分，添加以下模板：

```
$utils.toJson($utils.rds.toJsonObject($ctx.result)[1][0])
```

由于*语句*有两个 SQL 查询，我们需要指定从具有 `$utils.rds.toJsonString($ctx.result))[1][0])` 的数据库返回的矩阵中的第二个结果。

### Mutation.updatePet


在 Amazon AppSync 控制台的架构编辑器中，为其选择**附加解析器**。`updatePet(input: UpdatePetInput!): Pet`选择您的 **RDS 数据来源**。在**请求映射模板**部分，添加以下模板。

```
{
"version": "2018-05-29",
    "statements": [
        $util.toJson("update Pets set type=:TYPE, price=:PRICE WHERE id=:ID"),
        $util.toJson("select * from Pets WHERE id = :ID")
    ],
    "variableMap": {
        ":ID": "$ctx.args.input.id",
        ":TYPE": $util.toJson($ctx.args.input.type),
        ":PRICE": $util.toJson($ctx.args.input.price)
    }
}
```

在**响应映射模板**部分，添加以下模板。

```
$utils.toJson($utils.rds.toJsonObject($ctx.result)[1][0])
```

### Mutation.deletePet


在 Amazon AppSync 控制台的架构编辑器中，为其选择**附加解析器**。`deletePet(input: DeletePetInput!): Pet`选择您的 **RDS 数据来源**。在**请求映射模板**部分，添加以下模板。

```
{
"version": "2018-05-29",
    "statements": [
        $util.toJson("select * from Pets WHERE id=:ID"),
        $util.toJson("delete from Pets WHERE id=:ID")
    ],
    "variableMap": {
        ":ID": "$ctx.args.input.id"
    }
}
```

在**响应映射模板**部分，添加以下模板。

```
$utils.toJson($utils.rds.toJsonObject($ctx.result)[0][0])
```

### Query.getPet


既然已为您的架构创建变更，现在将连接三个查询以展示如何获取各个项目和列表以及应用 SQL 筛选。在 Amazon AppSync 控制台的**架构编辑器**中，为其选择**附加解析器**。`getPet(id: ID!): Pet`选择您的 **RDS 数据来源**。在**请求映射模板**部分，添加以下模板。

```
{
"version": "2018-05-29",
        "statements": [
            $util.toJson("select * from Pets WHERE id=:ID")
    ],
    "variableMap": {
        ":ID": "$ctx.args.id"
    }
}
```

在**响应映射模板**部分，添加以下模板：

```
$utils.toJson($utils.rds.toJsonObject($ctx.result)[0][0])
```

### Query.listPets


在 Amazon AppSync 控制台的架构编辑器中，在右侧选择 Att **ach Resolver fo** `getPet(id: ID!): Pet` r。选择您的 **RDS 数据来源**。在**请求映射模板**部分，添加以下模板。

```
{
    "version": "2018-05-29",
    "statements": [
        "select * from Pets"
    ]
}
```

在**响应映射模板**部分，添加以下模板。

```
$utils.toJson($utils.rds.toJsonObject($ctx.result)[0])
```

### 查询。 listPetsByPriceRange


在 Amazon AppSync 控制台的架构编辑器中，在右侧选择 Att **ach Resolver fo** `getPet(id: ID!): Pet` r。选择您的 **RDS 数据来源**。在**请求映射模板**部分，添加以下模板。

```
{
    "version": "2018-05-29",
    "statements": [
            "select * from Pets where price > :MIN and price < :MAX"
    ],

    "variableMap": {
        ":MAX": $util.toJson($ctx.args.max),
        ":MIN": $util.toJson($ctx.args.min)
    }
}
```

在**响应映射模板**部分，添加以下模板：

```
$utils.toJson($utils.rds.toJsonObject($ctx.result)[0])
```

## 通过 API 修改数据


现在，您已使用 SQL 语句配置所有解析器并将 GraphQL API 连接到 Serverless Aurora 数据 API，可以开始执行变更和查询。在 Amazon AppSync 控制台中，选择 **Quer** ies 选项卡并输入以下内容来创建 Pet：

```
mutation add {
    createPet(input : { type:fish, price:10.0 }){
        id
        type
        price
    }
}
```

该响应包含 *id*、*类型* 和*价格*，如下所示：

```
{
  "data": {
    "createPet": {
      "id": "c6fedbbe-57ad-4da3-860a-ffe8d039882a",
      "type": "fish",
      "price": "10.0"
    }
  }
}
```

您可以通过运行 *updatePet* 变更来修改此项目：

```
mutation update {
    updatePet(input : {
        id: ID_PLACEHOLDER,
        type:bird,
        price:50.0
    }){
        id
        type
        price
    }
}
```

请注意，我们使用了以前从 *createPet* 操作返回的 *id*。当解析器利用 `$util.autoId()` 时，这将是您的记录的唯一值。您可以通过类似的方式删除记录：

```
mutation delete {
    deletePet(input : {id:ID_PLACEHOLDER}){
        id
        type
        price
    }
}
```

使用包含不同的*价格* 值的第一个变更创建几条记录，然后运行几个查询。

## 检索数据


同样，在控制台的**查询**选项卡中，使用以下语句列出您创建的所有记录。

```
query allpets {
    listPets {
        id
        type
        price
    }
}
```

利用我们的映射模板`where price > :MIN and price < :MAX`中的 SQL WH *ER* E 谓词进行*查询。 listPetsByPriceRange*使用以下 GraphQL 查询：

```
query petsByPriceRange {
    listPetsByPriceRange(min:1, max:11) {
        id
        type
        price
    }
}
```

您应仅看到包含高于 1 美元或低于 10 美元的*价格* 的记录。最后，您可以执行查询以检索各个记录，如下所示：

```
query onePet {
    getPet(id:ID_PLACEHOLDER){
        id
        type
        price
    }
}
```

## 保护您的数据访问


SQL 注入是数据库应用程序中的一个安全漏洞。当攻击者通过用户输入字段插入恶意 SQL 代码时，就会发生注入。通过注入可允许未经授权访问数据库数据。我们建议您在使用 `variableMap` 进行处理之前仔细验证和清理所有用户输入，以防范 SQL 注入攻击。如果不使用变量映射，则由您负责清理其 GraphQL 操作的参数。执行此操作的一种方法是在对数据 API 执行 SQL 语句之前，在请求映射模板中提供特定于输入的验证步骤。让我们看看如何修改 `listPetsByPriceRange` 示例的请求映射模板。您可以执行以下操作，而不是仅依赖于用户输入：

```
#set($validMaxPrice = $util.matches("\d{1,3}[,\\.]?(\\d{1,2})?",$ctx.args.maxPrice))

#set($validMinPrice = $util.matches("\d{1,3}[,\\.]?(\\d{1,2})?",$ctx.args.minPrice))


#if (!$validMaxPrice || !$validMinPrice)
    $util.error("Provided price input is not valid.")
#end
{
    "version": "2018-05-29",
    "statements": [
            "select * from Pets where price > :MIN and price < :MAX"
    ],

    "variableMap": {
        ":MAX": $util.toJson($ctx.args.maxPrice),
        ":MIN": $util.toJson($ctx.args.minPrice)
    }
}
```

在对数据 API 执行解析器时防止恶意输入的另一种方法是，将预编译语句与存储过程和参数化输入一起使用。例如，在 `listPets` 的解析器中，定义以下将 *select* 作为预编译语句执行的过程：

```
CREATE PROCEDURE listPets (IN type_param VARCHAR(200))
  BEGIN
     PREPARE stmt FROM 'SELECT * FROM Pets where type=?';
     SET @type = type_param;
     EXECUTE stmt USING @type;
     DEALLOCATE PREPARE stmt;
  END
```

在您的 Aurora Serverless v2 实例中创建。

```
aws rds-data execute-statement --resource-arn "arn:aws:rds:us-east-1:xxxxxxxxxxxx:cluster:http-endpoint-test" \
--schema "mysql"  --secret-arn "arn:aws:secretsmanager:us-east-1:xxxxxxxxxxxx:secret:httpendpoint-xxxxxx"  \
--region us-east-1  --database "DB_NAME" \
--sql "CREATE PROCEDURE listPets (IN type_param VARCHAR(200)) BEGIN PREPARE stmt FROM 'SELECT * FROM Pets where type=?'; SET @type = type_param; EXECUTE stmt USING @type; DEALLOCATE PREPARE stmt; END"
```

由于我们现在只是调用了存储过程，因此简化了为 listPets 生成的解析器代码。至少，任何字符串输入都应该有单引号进行[转义](#escaped)。

```
#set ($validType = $util.isString($ctx.args.type) && !$util.isNullOrBlank($ctx.args.type))
#if (!$validType)
    $util.error("Input for 'type' is not valid.", "ValidationError")
#end

{
    "version": "2018-05-29",
    "statements": [
        "CALL listPets(:type)"
    ]
    "variableMap": {
        ":type": $util.toJson($ctx.args.type.replace("'", "''"))
    }
}
```

### 使用转义字符串


使用单引号来标记 SQL 语句中字符串文字的开始和结束，例如 `'some string value'`。要允许在字符串中使用具有一个或多个单引号字符 ( `'`) 的字符串值，必须用两个单引号 (`''`) 替换每个字符串值。例如，如果输入字符串是 `Nadia's dog`，您可以针对 SQL 语句将其转义，例如

```
update Pets set type='Nadia''s dog' WHERE id='1'
```

# 在中使用管道解析器 Amazon AppSync
使用管线解析器

**注意**  
我们现在主要支持 APPSYNC\$1JS 运行时系统及其文档。请考虑使用 APPSYNC\$1JS 运行时系统和[此处](https://docs.amazonaws.cn/appsync/latest/devguide/tutorials-js.html)的指南。

Amazon AppSync 提供了一种通过单位解析器将 GraphQL 字段连接到单个数据源的简单方法。但是，执行单个操作可能还不够。管道解析器提供了对数据来源连续执行操作的能力。在 API 中创建函数并将这些函数附加到管道解析器。每个函数执行结果将通过管道传输到下一个函数，直到没有要执行的函数为止。利用管道解析程序，您现在可直接在 Amazon AppSync 中构建更复杂的工作流程。在本教程中，您将构建一个简单的图片查看应用程序，用户可以在其中发布图片和查看其好友发布的图片。

## 一键设置


如果要在 Amazon AppSync 配置了所有解析器和必要 Amazon 资源的情况下自动设置 GraphQL 端点，则可以使用以下模板： Amazon CloudFormation 

[https://console.amazonaws.cn/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3.us-west-2.amazonaws.com/awsappsync/resources/pipeline/pipeline-resolvers-full.yaml](https://console.amazonaws.cn/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3.us-west-2.amazonaws.com/awsappsync/resources/pipeline/pipeline-resolvers-full.yaml)

此堆栈在您的账户中创建以下资源：
+ 用于 Amazon AppSync 访问您账户中资源的 IAM 角色
+ 2 个 DynamoDB 表
+ 1 个 Amazon Cognito 用户池
+ 2 个 Amazon Cognito 用户池组
+ 3 个 Amazon Cognito 用户池用户
+ 1 Amazon AppSync API

在 Amazon CloudFormation 堆栈创建过程结束时，您会收到一封针对已创建的三个 Amazon Cognito 用户的电子邮件。每封电子邮件都包含一个临时密码，您使用该密码以 Amazon Cognito 用户身份登录控制台。 Amazon AppSync 保存密码完成本教程的剩余部分。

## 手动设置


如果您更喜欢通过 Amazon AppSync控制台手动完成某个 step-by-step过程，请按照以下设置过程进行操作。

### 设置您的非 Amazon AppSync 资源


该 API 与两个 DynamoDB 表进行通信：**pictures** 表存储图片，**friends** 表存储用户之间的关系。此 API 配置为使用 Amazon Cognito 用户池作为身份验证类型。以下 Amazon CloudFormation 堆栈在账户中设置这些资源。

[https://console.amazonaws.cn/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3.us-west-2.amazonaws.com/awsappsync/resources/pipeline/pipeline-resolvers-resources-only.yaml](https://console.amazonaws.cn/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3.us-west-2.amazonaws.com/awsappsync/resources/pipeline/pipeline-resolvers-resources-only.yaml)

在 Amazon CloudFormation 堆栈创建过程结束时，您会收到一封针对已创建的三个 Amazon Cognito 用户的电子邮件。每封电子邮件均包含一个临时密码，您可使用此密码以 Amazon Cognito 用户身份登录 Amazon AppSync 控制台。保存密码完成本教程的剩余部分。

### 创建您的 GraphQL API


要在以下位置创建 GraphQL API，请执行以下操作： Amazon AppSync

1. 打开 Amazon AppSync 控制台，选择 “**从头开始构建**”，然后选择 “**开始**”。

1. 将 API 的名称设置为 `AppSyncTutorial-PicturesViewer`。

1. 选择**创建**。

 Amazon AppSync 控制台使用 API 密钥身份验证模式为您创建一个新的 GraphQL API。您可以根据本教程后面的说明，使用控制台设置 GraphQL API 的其余部分，并针对它运行查询。

### 配置 GraphQL API


您需要使用刚刚创建的 Amazon Cognito 用户池来配置 Amazon AppSync API。

1. 选择**设置**选项卡。

1. 在 **Authorization Type (授权类型)** 部分下，选择 *Amazon Cognito User Pool (Amazon Cognito 用户池)*。

1. 在**用户池配置**下面，为 *Amazon 区域*选择 **US-WEST-2**。

1. 选择 **AppSyncTutorial-UserPool** 用户池。

1. 选择 **DENY** 作为*默认操作*。

1. 将**AppId 客户端正则表达式**字段留空。

1. 选择**保存**。

此 API 现在设置为使用 Amazon Cognito 用户池作为其授权类型。

### 为 DynamoDB 表配置数据来源


**创建 DynamoDB 表后，在控制台中导航到您的 Amazon AppSync GraphQL API，然后选择 “数据源” 选项卡。**现在，你要在中 Amazon AppSync 为刚刚创建的每个 DynamoDB 表创建一个数据源。

1. 选择 **Data source (数据来源)** 选项卡。

1. 选择 **New (新建)** 创建新的数据来源。

1. 对于数据来源名称，输入 `PicturesDynamoDBTable`。

1. 对于数据来源类型，选择 **Amazon DynamoDB 表**。

1. 对于区域，选择 **US-WEST-2**。

1. 从表格列表中，选择 **AppSyncTutorial-Pic** tures DynamoDB 表。

1. 在**创建或使用现有角色**部分中，选择**现有角色**。

1. 选择刚刚根据 CloudFormation 模板创建的角色。如果您没有更改 *ResourceNamePrefix*，则该角色的名称应为 **AppSyncTutorial-Dynamo DBRole**。

1. 选择**创建**。

对**好友**表重复相同的过程，如果您在创建堆栈时没有更改*ResourceNamePrefix*参数，则 DynamoDB 表的名称**AppSyncTutorial应**为-Friends。 CloudFormation 

### 创建 GraphQL 架构


数据来源现已连接到您的 DynamoDB 表，让我们创建一个 GraphQL 架构。在 Amazon AppSync 控制台的架构编辑器中，确保您的架构与以下架构相匹配：

```
schema {
    query: Query
    mutation: Mutation
}

type Mutation {
    createPicture(input: CreatePictureInput!): Picture!
    @aws_auth(cognito_groups: ["Admins"])
    createFriendship(id: ID!, target: ID!): Boolean
    @aws_auth(cognito_groups: ["Admins"])
}

type Query {
    getPicturesByOwner(id: ID!): [Picture]
    @aws_auth(cognito_groups: ["Admins", "Viewers"])
}

type Picture {
    id: ID!
    owner: ID!
    src: String
}

input CreatePictureInput {
    owner: ID!
    src: String!
}
```

选择 **Save Schema (保存架构)** 以保存您的架构。

已使用 *@aws\$1auth* 指令对一些架构字段进行注释。由于 API 默认操作配置设置为 *DENY*，因此此 API 将拒绝不属于 *@aws\$1auth* 指令中提及的组成员的所有用户。有关如何保护您的 API 的更多信息，您可以阅读[安全性](security-authz.md#aws-appsync-security)页面。*在这种情况下，只有管理员用户才能访问 Mutation.createPict *ure 和 mutation.creat* *e* Friendship 字段，而属于*管理员*或查看者组成员的用户可以访问查询。* * getPicturesBy所有者*字段。所有其他用户都没有访问权限。

### 配置解析器


现在，您有一个有效的 GraphQL 架构和两个数据来源，可以将解析器附加到架构上的 GraphQL 字段。此 API 提供以下功能：
+ 通过 *Mutation.createPicture* 字段创建图片
+ 通过 *Mutation.createFriendship* 字段创建友好关系
+ 通过 *Query.getPicture* 字段检索图片

#### Mutation.createPicture


在 Amazon AppSync 控制台的架构编辑器中，在右侧选择 Att **ach Resolver fo** `createPicture(input: CreatePictureInput!): Picture!` r。选择 DynamoDB *PicturesDynamoDBTable*数据源。在**请求映射模板**部分，添加以下模板：

```
#set($id = $util.autoId())

{
    "version" : "2018-05-29",

    "operation" : "PutItem",

    "key" : {
        "id" : $util.dynamodb.toDynamoDBJson($id),
        "owner": $util.dynamodb.toDynamoDBJson($ctx.args.input.owner)
    },

    "attributeValues" : $util.dynamodb.toMapValuesJson($ctx.args.input)
}
```

在**响应映射模板**部分，添加以下模板：

```
#if($ctx.error)
    $util.error($ctx.error.message, $ctx.error.type)
#end
$util.toJson($ctx.result)
```

创建图片功能已完成。将图片保存在**图片**表中，使用随机生成的 UUID 作为图片的 ID 并使用 Cognito 用户名作为图片拥有者。

#### Mutation.createFriendship


在 Amazon AppSync 控制台的架构编辑器中，在右侧选择 Att **ach Resolver fo** `createFriendship(id: ID!, target: ID!): Boolean` r。选择 DynamoDB **FriendsDynamoDBTable**数据源。在**请求映射模板**部分，添加以下模板：

```
#set($userToFriendFriendship = { "userId" : "$ctx.args.id", "friendId": "$ctx.args.target" })
#set($friendToUserFriendship = { "userId" : "$ctx.args.target", "friendId": "$ctx.args.id" })
#set($friendsItems = [$util.dynamodb.toMapValues($userToFriendFriendship), $util.dynamodb.toMapValues($friendToUserFriendship)])

{
    "version" : "2018-05-29",
    "operation" : "BatchPutItem",
    "tables" : {
        ## Replace 'AppSyncTutorial-' default below with the ResourceNamePrefix you provided in the CloudFormation template
        "AppSyncTutorial-Friends": $util.toJson($friendsItems)
    }
}
```

重要：在**BatchPutItem**请求模板中，应显示 DynamoDB 表的确切名称。默认的表名是 *AppSyncTutorial-Fri* ends。如果您使用了错误的表名，则在 AppSync 尝试担任所提供的角色时会出现错误。

为了简化本教程，请像友谊请求已获批准一样继续操作，并将关系条目直接保存到**AppSyncTutorialFriends**表格中。

实际上，您将为每个友好关系存储两个项目，因为此关系是双向的。有关 many-to-many表现关系的 Amazon DynamoDB 最佳实践的更多详细信息，请参阅 D [ynamoD](https://docs.amazonaws.cn/amazondynamodb/latest/developerguide/bp-adjacency-graphs.html) B 最佳实践。

在**响应映射模板**部分，添加以下模板：

```
#if($ctx.error)
    $util.error($ctx.error.message, $ctx.error.type)
#end
true
```

注意：请确保请求模板包含正确的表名称。默认名称是 *AppSyncTutorial-Frien* ds，但是如果您更改了 CloudFormation **ResourceNamePrefix**参数，您的表名可能会有所不同。

#### 查询。 getPicturesBy所有者


现在，您已具有友好关系和图片，需要为用户提供查看其好友图片的功能。要满足此要求，您需要先确认请求者是拥有者的好友，最后查询图片。

由于此功能需要两个数据来源操作，因此您将创建两个函数。第一个函数 **isFriend** 将检查请求者和拥有者是否为好友。第二个函数 Owner 在给定**getPicturesBy所有者** ID 的情况下检索请求的照片。让我们来看看下面针对*查询中建议的解析器的执行流程。 getPicturesBy所有者*字段：

1. 之前映射模板：准备上下文和字段输入参数。

1. isFriend 函数：检查请求者是否为图片的拥有者。如果不是，它会通过在好友表上执行 D GetItem ynamoDB 操作来检查请求者和所有者用户是否是朋友。

1. getPicturesBy所有者函数：使用所有者索引全局二级索引上的 DynamoDB 查询操作从图片表*中*检索图片。

1. 之后映射模板：映射图片结果，以便 DynamoDB 属性能够正确地映射到所需的 GraphQL 类型字段。

让我们先创建函数。

##### isFriend 函数


1. 选择 **Functions (函数)** 选项卡。

1. 选择 **Create Function (创建函数)** 以创建函数。

1. 对于数据来源名称，输入 `FriendsDynamoDBTable`。

1. 对于函数名称，请输入 *isFriend*。

1. 在请求映射模板文本区域内，粘贴以下模板：

   ```
   #set($ownerId = $ctx.prev.result.owner)
   #set($callerId = $ctx.prev.result.callerId)
   
   ## if the owner is the caller, no need to make the check
   #if($ownerId == $callerId)
       #return($ctx.prev.result)
   #end
   
   {
       "version" : "2018-05-29",
   
       "operation" : "GetItem",
   
       "key" : {
           "userId" : $util.dynamodb.toDynamoDBJson($callerId),
           "friendId" : $util.dynamodb.toDynamoDBJson($ownerId)
       }
   }
   ```

1. 在响应映射模板文本区域内，粘贴以下模板：

   ```
   #if($ctx.error)
       $util.error("Unable to retrieve friend mapping message: ${ctx.error.message}", $ctx.error.type)
   #end
   
   ## if the users aren't friends
   #if(!$ctx.result)
       $util.unauthorized()
   #end
   
   $util.toJson($ctx.prev.result)
   ```

1. 选择**创建函数**。

结果：您创建了 **isFriend** 函数。

##### getPicturesBy所有者函数


1. 选择 **Functions (函数)** 选项卡。

1. 选择 **Create Function (创建函数)** 以创建函数。

1. 对于数据来源名称，输入 `PicturesDynamoDBTable`。

1. 对于函数名称，输入 `getPicturesByOwner`。

1. 在请求映射模板文本区域内，粘贴以下模板：

   ```
   {
       "version" : "2018-05-29",
   
       "operation" : "Query",
   
       "query" : {
           "expression": "#owner = :owner",
           "expressionNames": {
               "#owner" : "owner"
           },
           "expressionValues" : {
               ":owner" : $util.dynamodb.toDynamoDBJson($ctx.prev.result.owner)
           }
       },
   
       "index": "owner-index"
   }
   ```

1. 在响应映射模板文本区域内，粘贴以下模板：

   ```
   #if($ctx.error)
       $util.error($ctx.error.message, $ctx.error.type)
   #end
   
   $util.toJson($ctx.result)
   ```

1. 选择**创建函数**。

结果：您已创建**getPicturesBy所有者**函数。现在，函数已经创建完毕，请将管道解析器附加到*查询。 getPicturesBy所有者*字段。

在 Amazon AppSync 控制台的架构编辑器中，在右侧选择 Att **ach Resolver fo** `Query.getPicturesByOwner(id: ID!): [Picture]` r。在以下页面上，选择数据来源下拉列表下显示的 **Convert to pipeline resolver (转换为管道解析器)** 链接。对之前映射模板使用以下过程：

```
#set($result = { "owner": $ctx.args.id, "callerId": $ctx.identity.username })
$util.toJson($result)
```

在 **after mapping template (之后映射模板)** 部分中，使用以下过程：

```
#foreach($picture in $ctx.result.items)
    ## prepend "src://" to picture.src property
    #set($picture['src'] = "src://${picture['src']}")
#end
$util.toJson($ctx.result.items)
```

选择 **Create Resolver (创建解析器)**。您已成功附加您的首个管道解析器。在同一页上，添加您之前创建的两个函数。在函数部分中，选择 **Add A Function (添加函数)**，然后选择或键入第一个函数的名称 **isFriend**。按照与 Owner 函数相同的过程添加第二个**getPicturesBy函**数。确保 **isFriend** 函数首先出现在列表中，然后是**getPicturesBy所有者**函数。您可以使用向上和向下箭头在管道中重新排列函数的执行顺序。

现在，已创建管道解析器并且您已附加函数，下面让我们测试新创建的 GraphQL API。

## 测试您的 GraphQL API


首先，您需要通过使用创建的管理员用户执行一些变更来填充图片和友好关系。在 Amazon AppSync 控制台的左侧，选择 “**查询**” 选项卡。

### createPicture 变更


1. 在 Amazon AppSync 控制台中，选择 “**查询**” 选项卡。

1. 选择 **Login With User Pools (使用用户池登录)**。

1. 在模态中，输入 CloudFormation 堆栈创建的 Cognito 示例客户端 ID（例如 37solo6mmhh7k4v63cqdfgdg5d）。

1. 输入您作为参数传递给 CloudFormation 堆栈的用户名。默认值为 **nadia**。

1. 使用发送到您提供的电子邮件的临时密码作为 CloudFormation 堆栈的参数（例如 *UserPoolUserEmail*）。

1. 选择登录。现在，您应该会看到该按钮已重命名为 **Logout nadia**，或者您在创建 CloudFormation 堆栈时选择的任何用户名（即 *UserPoolUsername*）。

让我们发送一些 *createPicture* 变更来填充“图片”表。在控制台中执行以下 GraphQL 查询：

```
mutation {
  createPicture(input:{
    owner: "nadia"
    src: "nadia.jpg"
  }) {
    id
    owner
    src
  }
}
```

响应看上去应与下内容类似：

```
{
  "data": {
    "createPicture": {
      "id": "c6fedbbe-57ad-4da3-860a-ffe8d039882a",
      "owner": "nadia",
      "src": "nadia.jpg"
    }
  }
}
```

让我们再添加几张图片：

```
mutation {
  createPicture(input:{
    owner: "shaggy"
    src: "shaggy.jpg"
  }) {
    id
    owner
    src
  }
}
```

```
mutation {
  createPicture(input:{
    owner: "rex"
    src: "rex.jpg"
  }) {
    id
    owner
    src
  }
}
```

您已以管理员用户身份使用 **nadia** 添加三张图片。

### createFriendship 变更


让我们添加友好关系条目。在控制台中执行以下变更。

注意：您仍必须以管理员用户身份（默认管理员用户为 **nadia**）登录。

```
mutation {
  createFriendship(id: "nadia", target: "shaggy")
}
```

响应应该类似于：

```
{
  "data": {
    "createFriendship": true
  }
}
```

 **nadia** 和 **shaggy** 是好友。**rex** 与任何人都不是好友。

### getPicturesBy所有者查询


在此步骤中，以 **nadia** 用户身份使用 Cognito 用户池和本教程开头设置的凭证登录。以 **nadia** 身份检索 **shaggy** 拥有的图片。

```
query {
    getPicturesByOwner(id: "shaggy") {
        id
        owner
        src
    }
}
```

由于 **nadia** 和 **shaggy** 是好友，因此查询应返回对应的图片。

```
{
  "data": {
    "getPicturesByOwner": [
      {
        "id": "05a16fba-cc29-41ee-a8d5-4e791f4f1079",
        "owner": "shaggy",
        "src": "src://shaggy.jpg"
      }
    ]
  }
}
```

同样，如果 **nadia** 尝试检索自己的图片，也会成功。已对管道解析器进行了优化，以避免在这种情况下运行 GetItem isF **ri** end 操作。尝试以下查询：

```
query {
    getPicturesByOwner(id: "nadia") {
        id
        owner
        src
    }
}
```

如果您在 API 中启用日志记录（在 **Settings (设置)** 窗格中），将调试级别设置为 **ALL (所有)**，并再次运行相同的查询，则查询将返回字段执行的日志。通过查看日志，您可以确定 **isFriend** 函数是否在**请求映射模板**阶段提前返回：

```
{
  "errors": [],
  "mappingTemplateType": "Request Mapping",
  "path": "[getPicturesByOwner]",
  "resolverArn": "arn:aws:appsync:us-west-2:XXXX:apis/XXXX/types/Query/fields/getPicturesByOwner",
  "functionArn": "arn:aws:appsync:us-west-2:XXXX:apis/XXXX/functions/o2f42p2jrfdl3dw7s6xub2csdfs",
  "functionName": "isFriend",
  "earlyReturnedValue": {
    "owner": "nadia",
    "callerId": "nadia"
  },
  "context": {
    "arguments": {
      "id": "nadia"
    },
    "prev": {
      "result": {
        "owner": "nadia",
        "callerId": "nadia"
      }
    },
    "stash": {},
    "outErrors": []
  },
  "fieldInError": false
}
```

密*earlyReturnedValue*钥表示 *\$1return* 指令返回的数据。

****最后，尽管**雷克斯**是 Viewers Cognito UserPool Group 的成员，而且由于**雷克斯**与任何人都不是朋友，因此他将无法访问 **shagg** y 或 nadia 拥有的任何照片。****如果您以 **rex** 身份登录控制台并执行以下查询：

```
query {
    getPicturesByOwner(id: "nadia") {
        id
        owner
        src
    }
}
```

您将收到以下未经授权错误：

```
{
  "data": {
    "getPicturesByOwner": null
  },
  "errors": [
    {
      "path": [
        "getPicturesByOwner"
      ],
      "data": null,
      "errorType": "Unauthorized",
      "errorInfo": null,
      "locations": [
        {
          "line": 2,
          "column": 9,
          "sourceName": null
        }
      ],
      "message": "Not Authorized to access getPicturesByOwner on type Query"
    }
  ]
}
```

您已使用管道解析器成功实现复杂的授权。

# 对中的版本化数据源使用增量同步操作 Amazon AppSync
对版本控制的数据来源执行增量同步操作

**注意**  
我们现在主要支持 APPSYNC\$1JS 运行时系统及其文档。请考虑使用 APPSYNC\$1JS 运行时系统和[此处](https://docs.amazonaws.cn/appsync/latest/devguide/tutorials-js.html)的指南。

中的客户端应用程序通过将 GraphQL 响应本地缓存到应用程序中的磁盘来 Amazon AppSync 存储数据。 mobile/web 版本化的数据来源和 `Sync` 操作使客户能够使用单个解析器执行同步过程。这使客户端能够将其本地缓存与来自一个基本查询的结果（可能包含大量记录）混合，然后仅接收自上次查询以来更改的数据（*增量更新*）。通过允许客户端使用初始请求和另一个请求中的增量更新来执行缓存的基本组合，您可以将计算工作从客户端应用程序移至后端。这对于经常在联机状态和脱机状态之间切换的客户端应用程序来说效率更高。

为了实现增量同步，`Sync` 查询会对版本化数据来源使用 `Sync` 操作。当 Amazon AppSync 突变更改版本化数据源中的项目时，该更改的记录也将存储在 *Delta* 表中。您可以选择对其他版本化数据源使用不同的 *Delta* 表（例如，每种类型一个，每个域区域一个），或者对您的 API 使用单个 *Delta* 表。 Amazon AppSync 建议不要对多个*增量表使用单个 Delta* 表， APIs以免主键发生冲突。

此外，增量同步客户端还可以接收订阅作为参数，然后客户端协调订阅在脱机和联机转换之间重新连接和写入。增量同步通过自动恢复订阅（包括指数回退和通过各种网络错误情况的抖动重试）并将事件存储在队列中来执行此操作。然后，在合并队列中的任何事件之前运行适当的增量或基本查询，最后正常处理订阅。

[包括Amplify在内的客户端配置选项的文档可在Amplif DataStore y框架网站上找到。](https://aws-amplify.github.io/)本文档概述了如何设置版本化的 DynamoDB 数据来源和 `Sync` 操作，以便与增量同步客户端一起使用，实现最佳的数据访问。

## 一键设置


要在 Amazon AppSync 配置了所有解析器和必要 Amazon 资源的情况下自动设置 GraphQL 端点，请使用以下模板： Amazon CloudFormation 

[https://console.amazonaws.cn/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3.us-west-2.amazonaws.com/awsappsync/resources/deltasync/deltasync-v2-full.yaml](https://console.amazonaws.cn/cloudformation/home?region=us-west-2#/stacks/new?templateURL=https://s3.us-west-2.amazonaws.com/awsappsync/resources/deltasync/deltasync-v2-full.yaml) 

此堆栈在您的账户中创建以下资源：
+ 2 个 DynamoDB 表（基表和增量表）
+ 1 个带有 Amazon AppSync API 密钥的 API
+ 1 个 IAM 角色（具有 DynamoDB 表策略）

两个表用于将您的同步查询分区到另一个表中，此表在客户端处于脱机状态时充当错过事件的日志。为了保持增量表上的查询效率，[Amazon Dyn TTLs](https://docs.amazonaws.cn/amazondynamodb/latest/developerguide/TTL.html) amoDB 用于根据需要自动整理事件。可以根据您对数据来源的需求配置 TTL 时间（您可能希望这是 1 小时，1 天等）。

## 架构


为了演示 Delta Sync，示例应用程序在 DynamoDB 中创建了一个由 B *ase* 和 D *elta* 表支持的 Po *st* s 架构。 Amazon AppSync 自动将突变写入两个表。同步查询将根据情况从*基本* 或*增量* 表中拉取记录，并定义单个订阅，以说明客户端如何在其重新连接逻辑中利用它。

```
input CreatePostInput {
    author: String!
    title: String!
    content: String!
    url: String
    ups: Int
    downs: Int
    _version: Int
}

interface Connection {
  nextToken: String
  startedAt: AWSTimestamp!
}

type Mutation {
    createPost(input: CreatePostInput!): Post
    updatePost(input: UpdatePostInput!): Post
    deletePost(input: DeletePostInput!): Post
}

type Post {
    id: ID!
    author: String!
    title: String!
    content: String!
    url: AWSURL
    ups: Int
    downs: Int
    _version: Int
    _deleted: Boolean
    _lastChangedAt: AWSTimestamp!
}

type PostConnection implements Connection {
    items: [Post!]!
    nextToken: String
    startedAt: AWSTimestamp!
}

type Query {
    getPost(id: ID!): Post
    syncPosts(limit: Int, nextToken: String, lastSync: AWSTimestamp): PostConnection!
}

type Subscription {
    onCreatePost: Post
        @aws_subscribe(mutations: ["createPost"])
    onUpdatePost: Post
        @aws_subscribe(mutations: ["updatePost"])
    onDeletePost: Post
        @aws_subscribe(mutations: ["deletePost"])
}

input DeletePostInput {
    id: ID!
    _version: Int!
}

input UpdatePostInput {
    id: ID!
    author: String
    title: String
    content: String
    url: String
    ups: Int
    downs: Int
    _version: Int!
}

schema {
    query: Query
    mutation: Mutation
    subscription: Subscription
}
```

GraphQL 架构是标准的，但在继续之前需注意以下几点。首先，所有变更都将先自动写入*基本* 表，然后写入*增量* 表。*基本* 表是状态的可信中央来源，而 *增量* 表是您的日志。如果未传入 `lastSync: AWSTimestamp`，将对*基* 表运行 `syncPosts` 查询将数据加载到缓存中，以及作为*全局同步过程*定期运行以处理边缘情况，即，客户端离线时间超过您在*增量* 表中配置的 TTL 时间。如果您传入 `lastSync: AWSTimestamp`，则此 `syncPosts` 查询将针对*增量* 表运行并由客户端用于检索自它们上次脱机以后更改的事件。Amplify 客户端会自动传递该 `lastSync: AWSTimestamp` 值，并相应地保存到磁盘。

*Post* 上的 *\$1deleted* 字段用于 **DELETE** 操作。当客户端处于脱机状态并且从*基本* 表中删除记录时，此属性将通知客户端执行同步以移出其本地缓存中的项目。如果客户端脱机较长时间并且在客户端可以使用增量同步查询检索此值之前已删除该项目，则基本查询中的全局捕获事件（可在客户端中配置）将运行，并且会从缓存中删除该项目。此字段标记为可选，因为它仅在运行包含已删除项目的同步查询时返回值。

## 变更


对于所有突变， Amazon AppSync 在*基*表中Create/Update/Delete执行标准操作，并自动将更改记录在 *Delta* 表中。您可以通过修改数据来源上的 `DeltaSyncTableTTL` 值来减少或延长保留记录的时间。对于拥有高速数据的组织，短时间保留记录可能是有意义的。另外，如果您的客户端长时间处于脱机状态，则最好是将记录保留较长时间。

## 同步查询


*基本查询* 是未指定 `lastSync` 值的 DynamoDB 同步操作。对于许多组织而言，这是有效的，因为基本查询仅在启动时运行，之后将定期运行。

*增量查询* 是指定了 `lastSync` 值的 DynamoDB 同步操作。每当客户端从脱机状态恢复联机状态时，就会执行*增量查询*（只要基本查询周期时间未触发运行）。客户端会自动跟踪上次成功运行查询以同步数据的时间。

运行增量查询时，查询的解析器使用 `ds_pk` 和 `ds_sk`，仅查询自客户端上次执行同步以来发生更改的记录。客户端将存储相应的 GraphQL 响应。

有关执行同步查询的详细信息，请参阅 [同步操作文档](aws-appsync-conflict-detection-and-sync-sync-operations.md)。

## 示例


让我们首先调用一个 `createPost` 变更来创建一个项目：

```
mutation create {
  createPost(input: {author: "Nadia", title: "My First Post", content: "Hello World"}) {
    id
    author
    title
    content
    _version
    _lastChangedAt
    _deleted
  }
}
```

此变更的返回值如下所示：

```
{
  "data": {
    "createPost": {
      "id": "81d36bbb-1579-4efe-92b8-2e3f679f628b",
      "author": "Nadia",
      "title": "My First Post",
      "content": "Hello World",
      "_version": 1,
      "_lastChangedAt": 1574469356331,
      "_deleted": null
    }
  }
}
```

如果您检查*基本* 表的内容，将看到一条如下所示的记录：

```
{
  "_lastChangedAt": {
    "N": "1574469356331"
  },
  "_version": {
    "N": "1"
  },
  "author": {
    "S": "Nadia"
  },
  "content": {
    "S": "Hello World"
  },
  "id": {
    "S": "81d36bbb-1579-4efe-92b8-2e3f679f628b"
  },
  "title": {
    "S": "My First Post"
  }
}
```

如果您检查*增量* 表的内容，将看到一条如下所示的记录：

```
{
  "_lastChangedAt": {
    "N": "1574469356331"
  },
  "_ttl": {
    "N": "1574472956"
  },
  "_version": {
    "N": "1"
  },
  "author": {
    "S": "Nadia"
  },
  "content": {
    "S": "Hello World"
  },
  "ds_pk": {
    "S": "AppSync-delta-sync-post:2019-11-23"
  },
  "ds_sk": {
    "S": "00:35:56.331:81d36bbb-1579-4efe-92b8-2e3f679f628b:1"
  },
  "id": {
    "S": "81d36bbb-1579-4efe-92b8-2e3f679f628b"
  },
  "title": {
    "S": "My First Post"
  }
}
```

现在我们可以模拟一个*基本* 查询，客户端将运行该查询来组合其本地数据存储，并使用如下所示的 `syncPosts` 查询：

```
query baseQuery {
  syncPosts(limit: 100, lastSync: null, nextToken: null) {
    items {
      id
      author
      title
      content
      _version
      _lastChangedAt
    }
    startedAt
    nextToken
  }
}
```

此*基本* 查询的返回值如下所示：

```
{
  "data": {
    "syncPosts": {
      "items": [
        {
          "id": "81d36bbb-1579-4efe-92b8-2e3f679f628b",
          "author": "Nadia",
          "title": "My First Post",
          "content": "Hello World",
          "_version": 1,
          "_lastChangedAt": 1574469356331
        }
      ],
      "startedAt": 1574469602238,
      "nextToken": null
    }
  }
}
```

我们稍后会保存 `startedAt` 值来模拟*增量* 查询，但首先我们需要对表进行更改。让我们使用 `updatePost` 变更来修改我们现有的文章：

```
mutation updatePost {
  updatePost(input: {id: "81d36bbb-1579-4efe-92b8-2e3f679f628b", _version: 1, title: "Actually this is my Second Post"}) {
    id
    author
    title
    content
    _version
    _lastChangedAt
    _deleted
  }
}
```

此变更的返回值如下所示：

```
{
  "data": {
    "updatePost": {
      "id": "81d36bbb-1579-4efe-92b8-2e3f679f628b",
      "author": "Nadia",
      "title": "Actually this is my Second Post",
      "content": "Hello World",
      "_version": 2,
      "_lastChangedAt": 1574469851417,
      "_deleted": null
    }
  }
}
```

如果您现在检查*基本* 表的内容，则应该看到更新后的项目：

```
{
  "_lastChangedAt": {
    "N": "1574469851417"
  },
  "_version": {
    "N": "2"
  },
  "author": {
    "S": "Nadia"
  },
  "content": {
    "S": "Hello World"
  },
  "id": {
    "S": "81d36bbb-1579-4efe-92b8-2e3f679f628b"
  },
  "title": {
    "S": "Actually this is my Second Post"
  }
}
```

如果您现在检查*增量* 表的内容，则应看到两条记录：

1. 创建项目时的记录

1. 项目更新时间的记录。

新项目将如下所示：

```
{
  "_lastChangedAt": {
    "N": "1574469851417"
  },
  "_ttl": {
    "N": "1574473451"
  },
  "_version": {
    "N": "2"
  },
  "author": {
    "S": "Nadia"
  },
  "content": {
    "S": "Hello World"
  },
  "ds_pk": {
    "S": "AppSync-delta-sync-post:2019-11-23"
  },
  "ds_sk": {
    "S": "00:44:11.417:81d36bbb-1579-4efe-92b8-2e3f679f628b:2"
  },
  "id": {
    "S": "81d36bbb-1579-4efe-92b8-2e3f679f628b"
  },
  "title": {
    "S": "Actually this is my Second Post"
  }
}
```

现在，我们可以模拟*增量* 查询来检索客户端脱机时发生的修改。我们将使用从*基本* 查询返回的 `startedAt` 值发出请求：

```
query delta {
  syncPosts(limit: 100, lastSync: 1574469602238, nextToken: null) {
    items {
      id
      author
      title
      content
      _version
    }
    startedAt
    nextToken
  }
}
```

此*增量* 查询的返回值如下所示：

```
{
  "data": {
    "syncPosts": {
      "items": [
        {
          "id": "81d36bbb-1579-4efe-92b8-2e3f679f628b",
          "author": "Nadia",
          "title": "Actually this is my Second Post",
          "content": "Hello World",
          "_version": 2
        }
      ],
      "startedAt": 1574470400808,
      "nextToken": null
    }
  }
}
```