

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

# 在中使用带有数据 API 的 Aurora PostgreSQL AWS AppSync
<a name="aurora-serverless-tutorial-js"></a>

 

了解如何使用将你的 GraphQL API 连接到 Aurora PostgreSQL 数据库。 AWS AppSync这种集成使您能够通过 GraphQL 操作执行 SQL 查询和突变，从而构建可扩展、数据驱动的应用程序。 AWS AppSync 提供了一个数据源，用于对启用了数据 API 的 Amazon Aurora 集群执行 SQL 语句。您可以使用 AWS AppSync 解析器通过 GraphQL 查询、突变和订阅对数据 API 运行 SQL 语句。

在开始本教程之前，您应该对 AWS 服务和 GraphQL 概念有基本的了解。

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

**Topics**
+ [设置您的 Aurora PostgreSQL 数据库](#creating-clusters)
+ [创建数据库和表](#creating-db-table)
+ [创建 GraphQL 架构](#rds-graphql-schema)
+ [RDS 的解析器](#rds-resolvers)
+ [删除集群](#rds-delete-cluster)

## 设置您的 Aurora PostgreSQL 数据库
<a name="creating-clusters"></a>

在向添加 Amazon RDS 数据源之前 AWS AppSync，请执行以下操作。

1. 在 Aurora Serverless v2 集群上启用数据 API。

1. 使用配置密钥 AWS Secrets Manager

1. 使用以下 AWS CLI 命令创建集群。

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

这将为集群返回一个 ARN。创建集群后，必须使用以下 AWS CLI 命令添加一个 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-postgresql
```

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

使用以下 AWS CLI 命令检查集群状态。

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

通过 AWS Secrets Manager 控制台创建密钥， AWS CLI 或者使用上一步中的`USERNAME`和输入文件创建密`COMPLEX_PASSWORD`钥，如下所示：

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

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

```
aws secretsmanager create-secret \
    --name appsync-tutorial-rds-secret \
    --secret-string file://creds.json
```

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

## 创建数据库和表
<a name="creating-db-table"></a>

首先，创建一个名为 `TESTDB` 的数据库。在 PostgreSQL 中，数据库是存放表和其他 SQL 对象的容器。确认在将 Aurora Serverless v2 集群添加到 AWS AppSync API 之前已进行正确配置。首先，使用 `--sql` 参数创建 *TESTDB* 数据库，如下所示。

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

 如果运行而未出现错误，请使用 `create table` 命令添加两个表：

```
 aws rds-data execute-statement \
    --resource-arn "arn:aws:rds:us-east-1:111122223333 ISN:cluster:appsync-tutorial" \
    --secret-arn "arn:aws:secretsmanager:us-east-1:111122223333 ISN:secret:appsync-tutorial-rds-secret" \
    --database "testdb" \
    --sql 'create table public.todos (id serial constraint todos_pk primary key, description text not null, due date not null, "createdAt" timestamp default now());'

aws rds-data execute-statement \
    --resource-arn "arn:aws:rds:us-east-1:111122223333 ISN:cluster:appsync-tutorial" \
    --secret-arn "arn:aws:secretsmanager:us-east-1:111122223333 ISN:secret:appsync-tutorial-rds-secret" \
    --database "testdb" \
    --sql 'create table public.tasks (id serial constraint tasks_pk primary key, description varchar, "todoId" integer not null constraint tasks_todos_id_fk references public.todos);'
```

如果成功，则在 API 中将集群添加为数据来源。

## 创建 GraphQL 架构
<a name="rds-graphql-schema"></a>

现在，您的 Aurora Serverless v2 数据 API 正在使用已配置的表运行，我们将创建一个 GraphQL 架构。您可以通过使用 API 创建向导从现有数据库导入表配置，从而快速创建 API。

首先：

1. 在 AWS AppSync 控制台中，选择**创建 API**，然后选择从 **Amazon Aurora 集群开始**。

1. 指定 API 详细信息，例如 **API 名称**，然后选择要生成 API 的数据库。

1. 选择数据库。如果需要，请更新区域，然后选择您的 Aurora 集群和 *TESTDB* 数据库。

1. 选择您的密钥，然后选择**导入**。

1. 发现表后，更新类型名称。将 `Todos` 更改为 `Todo`，并将 `Tasks` 更改为 `Task`。

1. 通过选择**预览架构**来预览生成的架构。您的架构将如下所示：

   ```
   type Todo {
     id: Int!
     description: String!
     due: AWSDate!
     createdAt: String
   }
   
   type Task {
     id: Int!
     todoId: Int!
     description: String
   }
   ```

1. 对于该角色，您可以 AWS AppSync 创建一个新角色，也可以使用类似于以下策略的策略创建一个角色：

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

****  

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

------

   请注意，此策略中有两条您要授予角色访问权限的语句。第一个资源是您的 Aurora 集群，第二个资源是您的 AWS Secrets Manager ARN。

   选择**下一步**，查看配置详细信息，然后选择**创建 API**。您现在已拥有完全正常运行的 API。您可以在**架构**页面上查看 API 的完整详细信息。

## RDS 的解析器
<a name="rds-resolvers"></a>

API 创建流程会自动创建解析器来与我们的类型进行交互。**架构**页面中包含以下一些解析器。
+ 通过 `Mutation.createTodo` 字段创建 `todo`。
+ 通过 `Mutation.updateTodo` 字段更新 `todo`。
+ 通过 `Mutation.deleteTodo` 字段删除 `todo`。
+ 通过 `Query.getTodo` 字段获取单个 `todo`。
+ 通过 `Query.listTodos` 字段列出所有 `todos`。

您会发现该 `Task` 类型附带了类似的字段和解析器。让我们更仔细地看看一些解析器。

### Mutation.createTodo
<a name="createtodo"></a>

在 AWS AppSync 控制台的架构编辑器中，选择右`testdb`侧的`createTodo(...): Todo`。解析器代码使用 `rds` 模块中的 `insert` 函数动态创建向 `todos` 表中添加数据的插入语句。因为我们正在使用 Postgres，所以我们可以利用 `returning` 语句来取回插入的数据。

更新以下解析器以正确指定 `due` 字段的 `DATE` 类型。

```
import { util } from '@aws-appsync/utils';
import { insert, createPgStatement, toJsonObject, typeHint } from '@aws-appsync/utils/rds';

export function request(ctx) {
    const { input } = ctx.args;
    // if a due date is provided, cast is as `DATE`
    if (input.due) {
        input.due = typeHint.DATE(input.due)
    }
    const insertStatement = insert({
        table: 'todos',
        values: input,
        returning: '*',
    });
    return createPgStatement(insertStatement)
}

export function response(ctx) {
    const { error, result } = ctx;
    if (error) {
        return util.appendError(
            error.message,
            error.type,
            result
        )
    }
    return toJsonObject(result)[0][0]
}
```

保存解析器。类型提示将输入对象中的 `due` 正确标记为 `DATE` 类型。这允许 Postgres 引擎正确解释该值。接下来，更新您的架构以从 `CreateTodo` 输入中删除 `id`。因为我们的 Postgres 数据库可以返回生成的 ID，所以您可以依靠它来创建并以单个请求的形式返回结果，如下所示。

```
input CreateTodoInput {
    due: AWSDate!
    createdAt: String
    description: String!
}
```

进行更改并更新您的架构。前往**查询**编辑器，以向数据库添加一个项目，如下所示。

```
mutation CreateTodo {
  createTodo(input: {description: "Hello World!", due: "2023-12-31"}) {
    id
    due
    description
    createdAt
  }
}
```

您将获得以下结果。

```
{
  "data": {
    "createTodo": {
      "id": 1,
      "due": "2023-12-31",
      "description": "Hello World!",
      "createdAt": "2023-11-14 20:47:11.875428"
    }
  }
}
```

### Query.listTodos
<a name="listtodo"></a>

在控制台的架构编辑器中，在右侧选择 `listTodos(id: ID!): Todo` 旁边的 `testdb`。请求处理程序使用 select 实用程序函数在运行时动态生成请求。

```
export function request(ctx) {
    const { filter = {}, limit = 100, nextToken } = ctx.args;
    const offset = nextToken ? +util.base64Decode(nextToken) : 0;
    const statement = select({
        table: 'todos',
        columns: '*',
        limit,
        offset,
        where: filter,
    });
    return createPgStatement(statement)
}
```

我们想根据 `due` 日期筛选 `todos`。让我们更新解析器以将 `due` 值强制转换为 `DATE`。更新导入列表和请求处理程序，如下所示。

```
import { util } from '@aws-appsync/utils';
import * as rds from '@aws-appsync/utils/rds';

export function request(ctx) {
  const { filter: where = {}, limit = 100, nextToken } = ctx.args;
  const offset = nextToken ? +util.base64Decode(nextToken) : 0;

  // if `due` is used in a filter, CAST the values to DATE.
  if (where.due) {
    Object.entries(where.due).forEach(([k, v]) => {
      if (k === 'between') {
        where.due[k] = v.map((d) => rds.typeHint.DATE(d));
      } else {
        where.due[k] = rds.typeHint.DATE(v);
      }
    });
  }

  const statement = rds.select({
    table: 'todos',
    columns: '*',
    limit,
    offset,
    where,
  });
  return rds.createPgStatement(statement);
}

export function response(ctx) {
  const {
    args: { limit = 100, nextToken },
    error,
    result,
  } = ctx;
  if (error) {
    return util.appendError(error.message, error.type, result);
  }
  const offset = nextToken ? +util.base64Decode(nextToken) : 0;
  const items = rds.toJsonObject(result)[0];
  const endOfResults = items?.length < limit;
  const token = endOfResults ? null : util.base64Encode(`${offset + limit}`);
  return { items, nextToken: token };
}
```

在**查询**编辑器中，执行下列操作。

```
query LIST {
  listTodos(limit: 10, filter: {due: {between: ["2021-01-01", "2025-01-02"]}}) {
    items {
      id
      due
      description
    }
  }
}
```

### Mutation.updateTodo
<a name="updatetodo"></a>

您也可以对 `Todo` 执行 `update`。从**查询**编辑器中，我们来更新第一个 `Todo` 项目（`id` 为 `1`）。

```
mutation UPDATE {
  updateTodo(input: {id: 1, description: "edits"}) {
    description
    due
    id
  }
}
```

请注意，您必须指定要更新的项目的 `id`。您也可以指定一个条件，以仅更新符合特定条件的项目。例如，如果描述以 `edits` 开头，我们可能只想编辑该项目，如下所示。

```
mutation UPDATE {
  updateTodo(input: {id: 1, description: "edits: make a change"}, condition: {description: {beginsWith: "edits"}}) {
    description
    due
    id
  }
}
```

就像我们处理 `create` 和 `list` 操作的方式一样，我们可以更新解析器以将 `due` 字段强制转换为 `DATE`。将这些更改保存到 `updateTodo`，如下所示。

```
import { util } from '@aws-appsync/utils';
import * as rds from '@aws-appsync/utils/rds';

export function request(ctx) {
  const { input: { id, ...values }, condition = {}, } = ctx.args;
  const where = { ...condition, id: { eq: id } };

  // if `due` is used in a condition, CAST the values to DATE.
  if (condition.due) {
    Object.entries(condition.due).forEach(([k, v]) => {
      if (k === 'between') {
        condition.due[k] = v.map((d) => rds.typeHint.DATE(d));
      } else {
        condition.due[k] = rds.typeHint.DATE(v);
      }
    });
  }

  // if a due date is provided, cast is as `DATE`
  if (values.due) {
    values.due = rds.typeHint.DATE(values.due);
  }

  const updateStatement = rds.update({
    table: 'todos',
    values,
    where,
    returning: '*',
  });
  return rds.createPgStatement(updateStatement);
}

export function response(ctx) {
  const { error, result } = ctx;
  if (error) {
    return util.appendError(error.message, error.type, result);
  }
  return rds.toJsonObject(result)[0][0];
}
```

现在尝试使用一个条件进行更新：

```
mutation UPDATE {
  updateTodo(
    input: {
        id: 1, description: "edits: make a change", due: "2023-12-12"},
    condition: {
        description: {beginsWith: "edits"}, due: {ge: "2023-11-08"}})
    {
          description
          due
          id
        }
}
```

### Mutation.deleteTodo
<a name="deletetodo"></a>

您可以通过 `deleteTodo` 突变对 `Todo` 执行 `delete`。这就像 `updateTodo` 变更一样，您必须指定要删除的项目的 `id`，如下所示。

```
mutation DELETE {
  deleteTodo(input: {id: 1}) {
    description
    due
    id
  }
}
```

### 编写自定义查询
<a name="writing-custom-queries"></a>

我们已经使用 `rds` 模块实用程序来创建我们的 SQL 语句。我们还可以编写自己的自定义静态语句来与我们的数据库进行交互。首先，更新架构以从 `CreateTask` 输入中删除 `id` 字段。

```
input CreateTaskInput {
    todoId: Int!
    description: String
}
```

接下来，创建几个任务。任务与 `Todo` 有外键关系，如下所示。

```
mutation TASKS {
  a: createTask(input: {todoId: 2, description: "my first sub task"}) { id }
  b:createTask(input: {todoId: 2, description: "another sub task"}) { id }
  c: createTask(input: {todoId: 2, description: "a final sub task"}) { id }
}
```

在您的 `Query` 类型中创建一个名为 `getTodoAndTasks` 的新字段，如下所示。

```
getTodoAndTasks(id: Int!): Todo
```

向 `Todo` 类型添加一个 `tasks` 字段，如下所示。

```
type Todo {
    due: AWSDate!
    id: Int!
    createdAt: String
    description: String!
    tasks:TaskConnection
}
```

保存架构。从控制台的架构编辑器中，在右侧为 `getTodosAndTasks(id: Int!): Todo` 选择**附加解析器**。选择您的 Amazon RDS 数据来源。使用以下代码更新您的解析器。

```
import { sql, createPgStatement,toJsonObject } from '@aws-appsync/utils/rds';

export function request(ctx) {
    return createPgStatement(
        sql`SELECT * from todos where id = ${ctx.args.id}`,
        sql`SELECT * from tasks where "todoId" = ${ctx.args.id}`);
}

export function response(ctx) {
    const result = toJsonObject(ctx.result);
    const todo = result[0][0];
    if (!todo) {
        return null;
    }
    todo.tasks = { items: result[1] };
    return todo;
}
```

在此代码中，我们使用 `sql` 标签模板编写了一条 SQL 语句，我们可以在运行时安全地将动态值传递给该语句。`createPgStatement` 一次最多可以处理两个 SQL 请求。我们用它来为 `todo` 发送一个查询，并为 `tasks` 发送另一个查询。您可以使用 `JOIN` 语句或任何其它方法来完成此操作。其想法是能够编写您自己的 SQL 语句来实现您的业务逻辑。要在**查询**编辑器中使用查询，请执行以下操作。

```
query TodoAndTasks {
  getTodosAndTasks(id: 2) {
    id
    due
    description
    tasks {
      items {
        id
        description
      }
    }
  }
}
```

## 删除集群
<a name="rds-delete-cluster"></a>

**重要**  
删除集群是永久性的。在执行此操作之前，请仔细检查您的项目。

删除集群：

```
$ aws rds delete-db-cluster \
    --db-cluster-identifier appsync-tutorial \
    --skip-final-snapshot
```