GraphQL 是一种用于 HTTP API 的查询语言,提供了静态类型接口,可方便地表示当今复杂的数据层次结构。 使用 GraphQL 的一种方式是导入实现 GraphQL 服务器的库,并向其注册实现数据库接口的自定义解析器。 另一种方式是使用 GraphQL 云服务来实现 GraphQL 服务器,并注册无服务器云函数作为解析器。 云服务的众多好处中,最大的实际优势之一是解析器的独立性和可组合性。 例如,我们可以编写一个解析器连接到关系数据库,另一个连接到搜索数据库。
下面我们将考虑使用 Amazon Web Services (AWS) 的此类设置。具体来说,我们使用 AWS AppSync 作为 GraphQL 云服务,使用 AWS Lambda 运行关系数据库解析器,该解析器使用 Go 和 Ent 作为实体框架实现。 与 AWS Lambda 最流行的运行时 Nodejs 相比,Go 提供了更快的启动时间、更高的性能,并且在我看来,提供了更好的开发体验。 作为额外的补充,Ent 提出了一种创新方法,用于对关系数据库进行类型安全访问,我认为这在 Go 生态系统中是无与伦比的。 总之,将 Ent 与 AWS Lambda 作为 AWS AppSync 解析器一起运行,是应对当今苛刻 API 需求的极其强大的设置。
在接下来的部分中,我们将设置 AWS AppSync 中的 GraphQL 和运行 Ent 的 AWS Lambda 函数。 随后,我们提出一个集成 Ent 和 AWS Lambda 事件处理程序的 Go 实现,然后对 Ent 函数进行快速测试。 最后,我们将其注册为 AWS AppSync API 的数据源,并配置解析器,这些解析器定义了从 GraphQL 请求到 AWS Lambda 事件的映射。 请注意,本教程需要 AWS 账户和可公开访问的 Postgres 数据库的 URL,这可能会产生费用。
设置 AWS AppSync 架构
要在 AWS AppSync 中设置 GraphQL 架构,请登录您的 AWS 账户并通过导航栏选择 AppSync 服务。 AppSync 服务的登录页面应显示一个“Create API”按钮,点击该按钮可进入“Getting Started”页面:

从头开始使用 AWS AppSync
在显示“Customize your API or import from Amazon DynamoDB”的上方面板中,选择“Build from scratch”选项,并点击该面板所属的“Start”按钮。 您现在应该看到一个表单,可以在其中插入 API 名称。 对于本教程,我们输入“Todo”,参见下面的截图,然后点击“Create”按钮。

在 AWS AppSync 中创建新的 API 资源
创建 AppSync API 后,您应该看到一个登录页面,显示定义架构的面板、查询 API 的面板以及将 AppSync 集成到您的应用程序中的面板,如下面的截图所示。

AWS AppSync API 的登录页面
点击第一个面板中的“Edit Schema”按钮,并将之前的架构替换为以下 GraphQL 架构:
input AddTodoInput {
title: String!
}
type AddTodoOutput {
todo: Todo!
}
type Mutation {
addTodo(input: AddTodoInput!): AddTodoOutput!
removeTodo(input: RemoveTodoInput!): RemoveTodoOutput!
}
type Query {
todos: [Todo!]!
todo(id: ID!): Todo
}
input RemoveTodoInput {
todoId: ID!
}
type RemoveTodoOutput {
todo: Todo!
}
type Todo {
id: ID!
title: String!
}
schema {
query: Query
mutation: Mutation
}
替换架构后,会运行一个简短的验证,您应该能够点击右上角的“Save Schema”按钮,并看到以下视图:

AWS AppSync API 的最终 GraphQL 架构
如果我们向 AppSync API 发送 GraphQL 请求,由于没有解析器附加到架构,API 将返回错误。 我们将在通过 AWS Lambda 部署 Ent 函数后配置解析器。
详细解释当前的 GraphQL 架构超出了本教程的范围。
简而言之,GraphQL 架构通过 Query.todos 实现列表 todos 操作,通过 Query.todo 实现单个读取 todo 操作,通过 Mutation.createTodo 实现创建 todo 操作,通过 Mutation.deleteTodo 实现删除操作。
GraphQL API 类似于 /todos 资源的简单 REST API 设计,其中我们将使用 GET /todos、GET /todos/:id、POST /todos 和 DELETE /todos/:id。
有关 GraphQL 架构设计的详细信息,例如 Query 和 Mutation 对象的参数和返回,我遵循 GitHub GraphQL API 的实践。
设置 AWS Lambda
有了 AppSync API,我们的下一站是运行 Ent 的 AWS Lambda 函数。 为此,我们通过导航栏导航到 AWS Lambda 服务,这将引导我们到列出函数的 AWS Lambda 服务登录页面:

显示函数的 AWS Lambda 登录页面。
我们点击右上角的“Create function”按钮,并在上方面板中选择“Author from scratch”。 此外,我们将函数命名为“ent”,将运行时设置为“Go 1.x”,然后点击底部的“Create function”按钮。 然后我们应该看到“ent”函数的登录页面:

Ent 函数的 AWS Lambda 函数概述。
在审查 Go 代码并上传编译后的二进制文件之前,我们需要调整“ent”函数的一些默认设置。
首先,我们将默认处理程序名称从 hello 更改为 main,这等于编译后的 Go 二进制文件的文件名:

Ent 函数的 AWS Lambda 运行时设置。
其次,我们添加一个环境变量 DATABASE_URL,用于编码数据库网络参数和凭据:

Ent 函数的 AWS Lambda 环境变量设置。
要打开到数据库的连接,请传入 DSN,例如 postgres://username:password@hostname/dbname。
默认情况下,AWS Lambda 会加密环境变量,使它们成为提供数据库连接参数的快速安全机制。
或者,可以使用 AWS Secretsmanager 服务并在 Lambda 函数的冷启动期间动态请求凭据,从而允许(除其他外)轮换凭据。
第三种选择是使用 AWS IAM 来处理数据库授权。
如果您在 AWS RDS 中创建了 Postgres 数据库,默认用户名和数据库名是 postgres。
可以通过修改 AWS RDS 实例来重置密码。
设置 Ent 并部署 AWS Lambda
我们现在审查、编译并将数据库 Go 二进制文件部署到“ent”函数。 您可以在 bodokaiser/entgo-aws-appsync 找到完整的源代码。
首先,我们创建一个空目录并切换到该目录:
mkdir entgo-aws-appsync
cd entgo-aws-appsync
其次,我们初始化一个新的 Go 模块来包含我们的项目:
go mod init entgo-aws-appsync
第三,我们在引入 ent 依赖的同时创建 Todo 架构:
go run -mod=mod entgo.io/ent/cmd/ent new Todo
并添加 title 字段:
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
// Todo 保存 Todo 实体的架构定义。
type Todo struct {
ent.Schema
}
// Todo 的字段。
func (Todo) Fields() []ent.Field {
return []ent.Field{
field.String("title"),
}
}
// Todo 的边。
func (Todo) Edges() []ent.Edge {
return nil
}
最后,我们执行 Ent 代码生成:
go generate ./ent
使用 Ent,我们编写了一组解析器函数,这些函数实现了对 todos 的创建、读取和删除操作:
package resolver
import (
"context"
"fmt"
"strconv"
"entgo-aws-appsync/ent"
"entgo-aws-appsync/ent/todo"
)
// TodosInput 是 Todos 查询的输入。
type TodosInput struct{}
// Todos 查询所有 todos。
func Todos(ctx context.Context, client *ent.Client, input TodosInput) ([]*ent.Todo, error) {
return client.Todo.
Query().
All(ctx)
}
// TodoByIDInput 是 TodoByID 查询的输入。
type TodoByIDInput struct {
ID string `json:"id"`
}
// TodoByID 通过 id 查询单个 todo。
func TodoByID(ctx context.Context, client *ent.Client, input TodoByIDInput) (*ent.Todo, error) {
tid, err := strconv.Atoi(input.ID)
if err != nil {
return nil, fmt.Errorf("failed parsing todo id: %w", err)
}
return client.Todo.
Query().
Where(todo.ID(tid)).
Only(ctx)
}
// AddTodoInput 是 AddTodo 变异的输入。
type AddTodoInput struct {
Title string `json:"title"`
}
// AddTodoOutput 是 AddTodo 变异的输出。
type AddTodoOutput struct {
Todo *ent.Todo `json:"todo"`
}
// AddTodo 添加一个 todo 并返回它。
func AddTodo(ctx context.Context, client *ent.Client, input AddTodoInput) (*AddTodoOutput, error) {
t, err := client.Todo.
Create().
SetTitle(input.Title).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed creating todo: %w", err)
}
return &AddTodoOutput{Todo: t}, nil
}
// RemoveTodoInput 是 RemoveTodo 变异的输入。
type RemoveTodoInput struct {
TodoID string `json:"todoId"`
}
// RemoveTodoOutput 是 RemoveTodo 变异的输出。
type RemoveTodoOutput struct {
Todo *ent.Todo `json:"todo"`
}
// RemoveTodo 删除一个 todo 并返回它。
func RemoveTodo(ctx context.Context, client *ent.Client, input RemoveTodoInput) (*RemoveTodoOutput, error) {
t, err := TodoByID(ctx, client, TodoByIDInput{ID: input.TodoID})
if err != nil {
return nil, fmt.Errorf("failed querying todo with id %q: %w", input.TodoID, err)
}
err = client.Todo.
DeleteOne(t).
Exec(ctx)
if err != nil {
return nil, fmt.Errorf("failed deleting todo with id %q: %w", input.TodoID, err)
}
return &RemoveTodoOutput{Todo: t}, nil
}
对解析器函数使用输入结构允许映射 GraphQL 请求参数。 使用输出结构允许为更复杂的操作返回多个对象。
为了将 Lambda 事件映射到解析器函数,我们实现了一个处理程序,该处理程序根据事件中的 action 字段执行映射:
package handler
import (
"context"
"encoding/json"
"fmt"
"log"
"entgo-aws-appsync/ent"
"entgo-aws-appsync/internal/resolver"
)
// Action 指定事件类型。
type Action string
// 支持的事件操作列表。
const (
ActionMigrate Action = "migrate"
ActionTodos = "todos"
ActionTodoByID = "todoById"
ActionAddTodo = "addTodo"
ActionRemoveTodo = "removeTodo"
)
// Event 是事件处理程序的参数。
type Event struct {
Action Action `json:"action"`
Input json.RawMessage `json:"input"`
}
// Handler 处理支持的事件。
type Handler struct {
client *ent.Client
}
// 返回一个新的事件处理程序。
func New(c *ent.Client) *Handler {
return &Handler{
client: c,
}
}
// Handle 实现按操作处理事件。
func (h *Handler) Handle(ctx context.Context, e Event) (interface{}, error) {
log.Printf("action %s with payload %s\n", e.Action, e.Input)
switch e.Action {
case ActionMigrate:
return nil, h.client.Schema.Create(ctx)
case ActionTodos:
var input resolver.TodosInput
return resolver.Todos(ctx, h.client, input)
case ActionTodoByID:
var input resolver.TodoByIDInput
if err := json.Unmarshal(e.Input, &input); err != nil {
return nil, fmt.Errorf("failed parsing %s params: %w", ActionTodoByID, err)
}
return resolver.TodoByID(ctx, h.client, input)
case ActionAddTodo:
var input resolver.AddTodoInput
if err := json.Unmarshal(e.Input, &input); err != nil {
return nil, fmt.Errorf("failed parsing %s params: %w", ActionAddTodo, err)
}
return resolver.AddTodo(ctx, h.client, input)
case ActionRemoveTodo:
var input resolver.RemoveTodoInput
if err := json.Unmarshal(e.Input, &input); err != nil {
return nil, fmt.Errorf("failed parsing %s params: %w", ActionRemoveTodo, err)
}
return resolver.RemoveTodo(ctx, h.client, input)
}
return nil, fmt.Errorf("invalid action %q", e.Action)
}
除了解析器操作,我们还添加了一个迁移操作,这是公开数据库迁移的一种便捷方式。
最后,我们需要将 Handler 类型的实例注册到 AWS Lambda 库。
package main
import (
"database/sql"
"log"
"os"
"entgo.io/ent/dialect"
entsql "entgo.io/ent/dialect/sql"
"github.com/aws/aws-lambda-go/lambda"
_ "github.com/jackc/pgx/v4/stdlib"
"entgo-aws-appsync/ent"
"entgo-aws-appsync/internal/handler"
)
func main() {
// 使用 pgx 驱动程序打开数据库连接
db, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatalf("failed opening database connection: %v", err)
}
// 为 Postgres 数据库初始化 ent 数据库客户端
client := ent.NewClient(ent.Driver(entsql.OpenDB(dialect.Postgres, db)))
defer client.Close()
// 注册我们的事件处理程序以侦听 Lambda 事件
lambda.Start(handler.New(client).Handle)
}
每当 AWS Lambda 执行冷启动时,都会执行 main 的函数体。
冷启动后,Lambda 函数被认为是“热”的,只有事件处理程序代码被执行,这使得 Lambda 执行非常高效。
要编译和部署 Go 代码,我们运行:
GOOS=linux go build -o main ./lambda
zip function.zip main
aws lambda update-function-code --function-name ent --zip-file fileb://function.zip
第一个命令创建一个名为 main 的编译二进制文件。
第二个命令将二进制文件压缩为 ZIP 存档,这是 AWS Lambda 所需的。
第三个命令用新的 ZIP 存档替换名为 ent 的 AWS Lambda 的函数代码。
如果您使用多个 AWS 账户,您可能需要使用 --profile <your aws profile> 开关。
成功部署 AWS Lambda 后,在 Web 控制台中打开“ent”函数的“Test”选项卡,并使用“migrate”操作调用它:

使用“migrate”操作调用 Lambda
成功后,您应该收到绿色反馈框,并测试“todos”操作的结果:

使用“todos”操作调用 Lambda
如果测试执行失败,您很可能遇到数据库连接问题。
配置 AWS AppSync 解析器
成功部署“ent”函数后,我们还需要将 ent Lambda 注册为 AppSync API 的数据源,并配置架构解析器以将 AppSync 请求映射到 Lambda 事件。 首先,在 Web 控制台中打开我们的 AWS AppSync API,并移动到“Data Sources”,您可以在左侧的导航窗格中找到它。

注册到 AWS AppSync API 的数据源列表
点击右上角的“Create data source”按钮,开始将“ent”函数注册为数据源:

将 ent Lambda 注册为 AWS AppSync API 的数据源
现在,打开 AppSync API 的 GraphQL 架构,并在右侧的侧边栏中搜索 Query 类型。
点击 Query.Todos 类型旁边的“Attach”按钮:

在 AWS AppSync API 中为 todos Query 附加解析器
在 Query.todos 的解析器视图中,选择 Lambda 函数作为数据源,启用请求映射模板选项,

在 AWS AppSync API 中配置 todos Query 的解析器映射
并复制以下模板:
{
"version" : "2017-02-28",
"operation": "Invoke",
"payload": {
"action": "todos"
}
}
对其余的 Query 和 Mutation 类型重复相同的过程:
{
"version" : "2017-02-28",
"operation": "Invoke",
"payload": {
"action": "todo",
"input": $util.toJson($context.args.input)
}
}
{
"version" : "2017-02-28",
"operation": "Invoke",
"payload": {
"action": "addTodo",
"input": $util.toJson($context.args.input)
}
}
{
"version" : "2017-02-28",
"operation": "Invoke",
"payload": {
"action": "removeTodo",
"input": $util.toJson($context.args.input)
}
}
请求映射模板让我们构造用于调用 Lambda 函数的事件对象。
通过 $context 对象,我们可以访问 GraphQL 请求和身份验证会话。
此外,可以顺序排列多个解析器,并通过 $context 对象引用各自的输出。
原则上,也可以定义响应映射模板。
然而,在大多数情况下,返回响应对象“原样”就足够了。
使用查询浏览器测试 AppSync
测试 API 的最简单方法是使用 AWS AppSync 中的查询浏览器。 或者,可以在其 AppSync API 的设置中注册一个 API 密钥,并使用任何标准的 GraphQL 客户端。
首先让我们创建一个标题为 foo 的 todo:
mutation MyMutation {
addTodo(input: { title: "foo" }) {
todo {
id
title
}
}
}

使用 AppSync 查询浏览器执行“addTodo”变异
请求 todos 列表应返回一个标题为 foo 的 todo:
query MyQuery {
todos {
title
id
}
}

使用 AppSync 查询浏览器执行“addTodo”变异
通过 id 请求 foo todo 也应该工作:
query MyQuery {
todo(id: "1") {
title
id
}
}

使用 AppSync 查询浏览器执行“addTodo”变异
总结
我们成功部署了一个无服务器 GraphQL API,用于使用 AWS AppSync、AWS Lambda 和 Ent 管理简单的 todos。 特别是,我们提供了通过 Web 控制台配置 AWS AppSync 和 AWS Lambda 的分步说明。 此外,我们还讨论了如何构建 Go 代码的建议。
我们没有涵盖在 AWS 中测试和设置数据库基础设施。 这些方面在无服务器范式中比传统范式更具挑战性。 例如,当许多 Lambda 函数并行冷启动时,我们很快会耗尽数据库的连接池,需要一些数据库代理。 此外,我们需要重新思考测试,因为我们只能访问本地和端到端测试,因为我们无法轻松地隔离运行云服务。
尽管如此,所提出的 GraphQL 服务器很好地扩展到了现实世界应用程序的复杂需求,受益于无服务器基础设施和 Ent 愉快的开发体验。
有问题吗?需要入门帮助?欢迎加入我们的 Discord 服务器 或 Slack 频道。
- 订阅我们的 新闻通讯
- 在 Twitter 上关注我们
- 加入 Gophers Slack 上的 #ent 频道
- 加入 Ent Discord 服务器