【Go语言】初次实现GraphQL服务器 | gqlgen

关于这篇文章

本文将介绍如何使用Go语言开发GraphQL服务器。

在这篇文章中提及的内容

    • GraphQLのスキーマ定義

 

    • Go言語でGraphQLサーバの開発方法

フレームワークとしてgqlgenを利用

エラー処理
負荷対策

N+1問題
クエリの大きさ判定

这篇文章没有提到的内容是什么。

    • Go言語の説明

 

    • GraphQLの概要について

 

    バックエンドのデータベース(MySQL等)への接続方法

源代码

这篇文章介绍的代码已放置在Github上。
https://github.com/hiroyky/go_graphql_server_sample

前提

    • Go言語の基本的な知識があり、お使いのマシンでGo言語のビルドや実行ができる。

 

    GraphQLの概要について知識がある。

GraphQL的概述有详细的官方说明。
同时,截至2021年11月23日,我们正在使用Go-1.17进行测试。

项目的开始

我将使用GraphQL框架gqlgen开始构建GraphQL服务器。

初始化

我們會按順序執行命令,在新的倉庫中初始化一個Go語言項目並引入GraphQL框架gqlgen。

$ go mod init
$ go get github.com/99designs/gqlgen
$ go run github.com/99designs/gqlgen init

通过这个,下面的文件组被创建了。

├── gqlgen.yml: コード自動生成の設定ファイル
├── graph
│   ├── generated
│   │   └── generated.go // 編集しないファイル
│   ├── model
│   │   └── models_gen.go // GraphQLで使う型が定義されているファイル
│   ├── resolver.go // リゾルバの定義
│   ├── schema.graphqls // GraphQLのスキーマ定義ファイル
│   └── schema.resolvers.go // 実際の処理を書くファイル
└── server.go // HTTPサーバの起動プログラム

即使在这个阶段,只要执行以下操作,就可以在 http://localhost:8080/ 打开草稿场界面。

$ go mod tidy
$ go run ./server.go
image.png

我已在这里部署了此内容。
https://github.com/hiroyky/go_graphql_server_sample/pull/1

GraphQL服务器开发的流程

刚才的自动生成中,以下3个文件也已经实施,内容如下:

schema.graphqlsにTODOリストを模したスキーマが定義されています。

model/model_gen.goにGo言語でその型定義が記述されています。

schema.resolvers.goに処理を実装するための雛形が記載されています。

创建程序的步骤如下:

    1. 1. 在.graphqls文件中编写GraphQL的模式定义。

 

    1. 执行gqlgen自动生成命令(gqlgen)。

 

    1. 检查已创建/更新的model/model_go.go文件,并根据需要将其移动和编辑到其他文件中。

2. 在.resolvers.go文件中实现处理体。

*.graphqls文件和*.resolvers.go文件是一对的。

GraphQL的模式定义(复习)

GraphQL的模式定义包括数据类型定义和函数(方法)定义。两者的定义方式与Go语言和其他语言的结构体和类的定义相同。

在类型定义中,我们首先确定结构体的名称,然后定义其内部的字段。作为字段类型的基本形式,我们提供了String、Int和Boolean。此外,还提供了表示主键的ID、枚举对、接口以及自定义类型定义的机制。

接口用于定义共享字段。实现了接口的类型必须具备该字段。可以说,接口在编程语言中与实际相当。

函数(方法)的定义可以分为两部分:查询(query)用于定义获取数据的函数,而变更(mutation)用于定义更新数据的函数。两者都需要定义函数名、参数和返回值。

公式说明”模式和类型”

模式定义和代码生成

“以公司、部门和员工为主题设计” (Yǐ , hé

我们可以考虑以自动生成的TODO清单模式为题材,但在这里,我想用公司(company)和部门(department),以及职员(employee)这个常见的题材来思考。如果用MySQL数据库表的ER图来表示题材的话,如下所示。在本文中,我们不涉及与后端数据库的交互,但假设我们正在使用MySQL进行讨论。

image.png

删除graph/model/schema.graphqls和graph/model/scheme.resolvers.go,并新建一个新的模式定义文件。

$ rm ./graph/model/schema.graphqls
$ rm ./graph/model/schema.resovers.go

定义1个基本的架构

由于我们是刚开始,所以让我们按照基本的类型定义部分的顺序进行。所有这些都将放置在graph/目录下。

最初,我们定义了一个在整个系统中共享的接口Node。按照惯例,具有主键id的类型应该实现Node接口。我们还定义了一个自定义类型Timestamp。类型末尾的感叹号表示它是必需的(不允许空值)。

interface Node {
    id: ID!
}

scalar Timestamp

那么让我们立刻开始定义Interface Node的实现,其中包括Company(公司),Department(部门)和Employee(员工)的类型。在Employee中,我们还定义了Gender这个枚举类型。(尽管最近对于性别,有人提出了除了男性和女性之外的更多选择,但在这里我们先不考虑)。

type Company implements Node {
    id: ID!
    companyName: String!
    representative: String!
    phoneNumber: String!
}
type Department implements Node {
    id: ID!
    departmentName: String!
    email: String!
}
type Employee implements Node {
    id: ID!
    name: String!
    gender: Gender!
    email: String!
    latestLoginAt: Timestamp!
    """ 扶養家族の人数 """
    dependentsNum: Int!
    """ 管理職かどうか """
    isManager: Boolean!
}

enum Gender {
    Male
    Female
}

在Go语言和其他语言中,定义成员变量的结构和类几乎是相同的。

定义与其他类型关联的2个架构

接下来,我们来进一步将类型之间的关联也纳入定义中。在GraphQL的类型定义中,我们可以为关系数据库中的关系添加关联。

首先,我們從員工(Employee)開始進行編輯。添加部門(department)和公司(company)欄位。這樣就可以追蹤員工(Employee)所屬的部門(Department)和公司(Company)。對了,資料庫的實體關係圖(ER diagram)中有部門編號(department_id)和公司編號(company_id)。

type Employee implements Node {
    id: ID!
    name: String!
    gender: Gender!
    email: String!
    latestLoginAt: Timestamp!
    """ 扶養家族の人数 """
    dependentsNum: Int!
    """ 管理職かどうか """
    isManager: Boolean!
    department: Department! # 追記
    company: Company!       # 追記
}

enum Gender {
    Male
    Female
}

定义3 [分页] 的模式

接下来,我打算在Department和Company中添加与关联表相关的链接。从Company到Department和Employee是一对多的关系,因此我们将使用数组来返回值。然而,不能每次都将所有数据一次性返回作为API,所以我们要定义分页功能。(如果设计成一次性返回所有数据的话,数据库会因为负载而崩溃的,请注意。)

这次我们将采用常见的Limit和Offset方式进行分页。在许多关系型数据库中,通过limit和offset来指定获取的数据量和位置,与此类似。虽然GraphQL在一些情况下也采用基于游标的分页方式,但本次不会使用。

以个人角度而言,我认为游标式分页适用于类似Twitter和Facebook时间线这样不断插入记录的数据,但对于其他类型的数据,我们应该考虑采用基于Limit和Offset的分页,而不是强制采用游标式分页。在这个例子中,由于公司和员工名单并非时刻变化的数据,我认为采用基于Limit和Offset的分页更加合适。

在准备分页定义时,将以下内容添加到common.graphqls中。

interface Pagination {
    pageInfo: PaginationInfo!
    nodes: [Node!]! # Node型の配列という意味
}

type PaginationInfo {
    page: Int!
    paginationLength: Int!
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
    count: Int!
    totalCount: Int!
}

那么,我们将为部门添加关联。在EmployeePagination中,我们使用字段名nodes来定义页码信息和Employee数组。

# 下記を追記
type EmployeePagination implements Pagination {
    pageInfo: PaginationInfo!
    nodes: [Employee!]!
}
type Department implements Node {
    id: ID!
    departmentName: String!
    email: String!
    company: Company!              # 追記
    employees: EmployeePagination! # 追記
}

同样,我会为公司添加相关的链接。

# 下記を追記
type DepartmentPagination implements Pagination {
    pageInfo: PaginationInfo!
    nodes: [Department!]!
}
type Company implements Node {
    id: ID!
    companyName: String!
    representative: String!
    phoneNumber: String!
    departments: DepartmentPagination! # 追記
    employees: EmployeePagination!     # 追記
}
# 後ほど使うので併せて定義
type CompanyPagination implements Pagination{
    pageInfo: PaginationInfo!
    nodes: [Company!]!
}

定义4个[查询和变更]模式

到目前为止,我们只是在进行类型定义,现在要开始定义函数(方法)。我们将定义一个获取查询类(query)和一个更新类(mutation)。这些需要分别用Query和Mutation来包装。函数的定义方法与常见的编程语言相似,所以应该能够直观地理解。如果参数上有一个感叹号(!),表示它是必需的;如果返回值上有一个感叹号(!),表示保证响应不会为NULL。

関数名(引数1: 引数の型1!, 引数2: 引数の型2..): 戻り値の型

在查询中,为每种类型定义了单数和复数的获取函数。这些函数都可以通过limit和offset参数来指定获取的数量和位置。需要注意的是,limit是必需的。在employees中,还定义了其他筛选条件。

type Query {
    company(id: ID!): Company
    companies(limit: Int!, offset: Int): CompanyPagination!
    department(id: ID!): Department
    departments(limit: Int!, offset: Int): DepartmentPagination!
    employee(id: ID!): Employee
    employees(
        limit: Int!,
        offset: Int,
        email: String
        gender: Gender,
        isManager: Boolean,
        hasDependent: Boolean
    ): EmployeePagination!
}

将更新系的变异定义在mutation.graphqls中。变异的参数是作为input整合的。参数的类型定义是用input Xxxx {}来描述,而不是用type。返回值的定义是,如果创建或更新成功,则返回新的值;如果删除操作,则返回一个固定的true值。

type Mutation {
    createCompany(input: CreateCompanyInput!): Company!
    updateCompany(input: UpdateCompanyInput!): Company!
    deleteCompany(id: ID!): Boolean!
    createDepartment(input: CreateDepartmentInput!): Department!
    updateDepartment(input: UpdateDepartmentInput!): Department!
    deleteDepartment(id: ID!): Boolean!
    createEmployee(input: CreateEmployeeInput!): Employee!
    updateEmployee(input: UpdateEmployeeInput!): Employee!
    deleteEmployee(id: ID!): Boolean!
}

input CreateCompanyInput {
    companyName: String!
    representative: String!
    phoneNumber: String!
}

input UpdateCompanyInput {
    id: ID!
    companyName: String
    representative: String
    phoneNumber: String
}

input CreateDepartmentInput {
    departmentName: String!
    email: String!
}

input UpdateDepartmentInput {
    id: ID!
    departmentName: String
    email: String
}

input CreateEmployeeInput {
    name: String!
    gender: Gender!
    email: String!
    dependentsNum: Int!
    isManager: Boolean!
}

input UpdateEmployeeInput {
    id: ID!
    name: String
    gender: Gender
    email: String
    dependentsNum: Int
    isManager: Boolean
}

我已将到目前为止的更新整理到了Github的Pull Request中。分支名称是feat2。
链接:https://github.com/hiroyky/go_graphql_server_sample/pull/2/files

代码生成

执行代码生成1 [使用gqlgen]

一旦完成了模式定义,可以使用gqlgen命令根据模式定义自动生成Go语言程序。

$ go run github.com/99designs/gqlgen

我认为Go语言的文件已按以下方式结构化生成。

./graph
├── generated
│   └── generated.go
├── model
│   └── models_gen.go
├── mutations.resolvers.go
├── query.resolvers.go
└── resolver.go

请问在query.resolvers.go和mutations.resolvers.go文件中,我们已经用Go语言定义了之前提到的函数。
在参数定义中,我们使用!来将参数标记为必需的值类型,而非必需的参数则采用了指针类型。如果未指定参数,则其值将为NULL。

// ・・・・
func (r *queryResolver) Employee(ctx context.Context, id string) (*model.Employee, error) {
    panic(fmt.Errorf("not implemented"))
}

func (r *queryResolver) Employees(ctx context.Context, limit int, offset *int, email *string, gender *model.Gender, isManager *bool, hasDependent *bool) (*model.EmployeePagination, error) {
    panic(fmt.Errorf("not implemented"))
}
// ・・・・

通过编辑结构体并重新生成来生成代码2。

顺便提一下,让我们来确认一下在model/models_gen.go文件中定义的类型。Employee类型的定义如下所示。该定义还反映了在模式定义中写的注释呢。

type Employee struct {
    ID            string `json:"id"`
    Name          string `json:"name"`
    Gender        Gender `json:"gender"`
    Email         string `json:"email"`
    LatestLoginAt string `json:"latestLoginAt"`
    //  扶養家族の人数
    DependentsNum int `json:"dependentsNum"`
    //  管理職かどうか
    IsManager  bool        `json:"isManager"`
    Department *Department `json:"department"`
    Company    *Company    `json:"company"`
}

现在,我们需要着眼于Department和Company这两个概念。乍一看似乎没有问题,但实际上存在问题。
如果在后端数据库中使用MySQL等关系型数据库,那么Department和Company不是直接作为数据库表格的内容,而是成为了外键的ID,比如department_id和company_id。当需要获取Company或者Department的内容时,就需要单独执行SQL查询或者执行包含连接操作的SQL查询。在GraphQL中,我们可以根据请求查询中是否包含对Department或Company的指定,灵活地执行需单独的SQL查询。我认为这是一个可能引起困惑的地方,所以让我们一起试一试吧。

image.png

将自动生成的模型类型更改。由于models/models_gen.go每次都会被重新生成,因此不会直接编辑此文件。相反,将创建一个新的models/models.go文件,放在相同的目录(包)中,并按照以下方式进行描述。同时删除models_gen.go中的Employee结构体。
需要注意的是,删除了Department *Department和Company *Company,取而代之的是定义DepartmentID string和CompanyID string。这样离数据库表的定义更近了。也许会有人听到ID应该是int类型的声音,但我们选择了string类型。原因将在后文中说明。

package model

type Employee struct {
    ID            string `json:"id"`
    Name          string `json:"name"`
    Gender        Gender `json:"gender"`
    Email         string `json:"email"`
    LatestLoginAt string `json:"latestLoginAt"`
    //  扶養家族の人数
    DependentsNum int `json:"dependentsNum"`
    //  管理職かどうか
    IsManager    bool   `json:"isManager"`
    DepartmentID string `json:"department"` // Departmentを削除して、代わりにDepartmentIDを記述
    CompanyID    string `json:"company"`    // Companyを削除して、代わりにCompanyIDを記述
}
func (Employee) IsNode() {}

完成了到这个地方的编辑之后,请再次运行gqlgen命令。

$ go run github.com/99designs/gqlgen

那么会发生什么呢?现在新生成了一个employee.resolvers.go文件。这里定义了两个函数。第二个函数是接收一个Employee作为第二个参数,并返回Department和Company的函数。
这是因为刚才我们自己重新定义的Employee在gqlgen中发现缺少了Department和Company,所以定义了这个函数来获取缺失的部分。函数的第二个参数是接收父对象Employee,我们可以利用它来获取子对象Company和Department的处理。

func (r *employeeResolver) Department(ctx context.Context, obj *model.Employee) (*model.Department, error) {
    panic(fmt.Errorf("not implemented"))
}

func (r *employeeResolver) Company(ctx context.Context, obj *model.Employee) (*model.Company, error) {
    panic(fmt.Errorf("not implemented"))
}

只有当客户端在Employee查询中指定了department或company时,这些函数才会被执行。如果不需要,它们就不会被调用。例如,在下面的请求查询中,会调用department(),但不会调用company(),因为没有指定它。

query {
   employee(id:"RW1wbG95ZWU6MQ==") {
      id
      name
      department {
         id
         departmentName
      }
   }
}

好的,现在我们来对其他Company和Department的结构体进行类似的编辑。我们将把它们从models_gen.go移动到models.go文件中,并进行添加和删除。

type Company struct {
    ID             string `json:"id"`
    CompanyName    string `json:"companyName"`
    Representative string `json:"representative"`
    PhoneNumber    string `json:"phoneNumber"`
    // Departmentsのフィールド自体を削除
    // Employeesのフィールド自体を削除
}
func (Company) IsNode() {}

type Department struct {
    ID             string `json:"id"`
    DepartmentName string `json:"departmentName"`
    Email          string `json:"email"`
    CompanyID      string `json:"company"` // Companiesを削除して、CompanyIDを追記
    // Employeesのフィールド自体を削除
}
func (Department) IsNode() {}

完成编辑后,请重新运行gqlgen命令。

$ go run github.com/99designs/gqlgen

刚刚像之前一样,在结构体和GraphQL模式定义中发现了差异的部分,并由gqlgen生成了补充缺失部分的函数。举个例子,新创建的companies.resolvers.go文件中定义了一个接收父亲类型Company作为第二个参数,并返回DepartmentPagination和EmployeePagination的函数。


func (r *companyResolver) Departments(ctx context.Context, obj *model.Company) (*model.DepartmentPagination, error) {
    panic(fmt.Errorf("not implemented"))
}

func (r *companyResolver) Employees(ctx context.Context, obj *model.Company) (*model.EmployeePagination, error) {
    panic(fmt.Errorf("not implemented"))
}

我将目前的编辑整理到了一个Pull Request中。分支名为feat3。
https://github.com/hiroyky/go_graphql_server_sample/pull/3

中身的实现和主键的注意事项

好的,现在让我们来实现自动生成的解析器函数的具体功能。这些功能包括连接后端数据库,发送请求至其他REST API等等。由于本文的重点不在于数据库和REST API的通信,所以不进行详细描述。请您自行进一步完成具体功能的实现。

然而,有一点需要注意。那就是要跨越类型使主键ID在全球范围内唯一。也就是说,雇员的ID是1、2、3…连续编号,部门的ID也是1、2、3…连续编号,但是ID在全球范围内不唯一。因此,主键应该按照以下方式用类型名称和集合来描述。

    • Employee:1

 

    • Employee:2

 

    • Employee:3

 

 

    • Department:1

 

    • Department:2

 

    • Department:3

 

通常情况下,GraphQL将Base64编码后的内容作为主键处理。

    • base64(Employee:1) = “RW1wbG95ZWU6MQ==”

 

    base64(Department:1) = “RGVwYXJ0bWVudDox

为了实现这一点,我们将结构体的ID从int类型改为了string类型。即使在后端数据库(如MySQL)中,使用int的连续编号来管理主键,但在GraphQL的响应和请求参数中,应该将其转换为与类型名称组合在一起的字符串(类型名称:编号)的base64字符串。原因是,在GraphQL中,仅凭ID本身就可以管理缓存等内容,以避免ID的重复。因此,如果主键使用了像UUID这样本身就具备唯一性的形式,那么就不需要进行转换。
有关全局对象标识(Global Object Identification)的解释可参考。

错误处理

在某些情况下,需要使用解析器生成错误并返回错误响应。 gqlgen框架也具备此功能。

GraphQL的响应JSON,在正常情况下只返回`data`,但在错误响应中会返回一个`errors`数组。错误内容包括以下项目:

    • message: エラーの内容を簡潔に伝えるメッセージ

 

    • path: エラーが起こった場所(クエリの位置)

 

    extensions: それ以外にクライアントに伝えたい内容があればkey:value形式で記述
{
    "data": {},
    "errors": [
        { 
            "message": "Error: hoge fuga", 
            "path": [ "employee" ], 
            "extensions": {"key1": "value1"} 
        },
        { "message": "Error: foo bar", "path": [ "department" ] },
    ]
}

为了实现这个,我们需要编辑server.go中的graphql服务器。我们将在服务器实例的SetErrorPresenter参数中编写一个生成错误响应的函数。将resolver等中抛出的错误传递到参数的error中,然后基于它生成错误响应。在此函数中返回gqlerror.Error类型。

    srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))
    // エラー処理を書く
    srv.SetErrorPresenter(func(ctx context.Context, e error) *gqlerror.Error {
        err := graphql.DefaultErrorPresenter(ctx, e)
        err.Message = e.Error()
        err.Extensions = map[string]interface{}{
            "key1": "value1",
            "key2": "value2",
        }
        return err
    })

我已经在GitHub上创建了一个pull request。分支名称是feat4。
可以从以下链接查看相关内容:
https://github.com/hiroyky/go_graphql_server_sample/pull/4

公式文献链接:https://gqlgen.com/reference/errors/

应对负荷问题

N+1问题的对策

总结

在GraphQL中,存在着N+1问题作为一个普遍问题。当查询引用子元素时,每次解析器都会被执行,这会导致后端数据库或API发生大量的请求现象。

例如,如果我们写一个查询来从Employee数组中引用所属的Company。这时,company()解析器将被每次执行(最多100次)。因此,由于每次都会发生请求到后端数据库等,数据库的负载会变得非常重。

query {
  employees(limit:100) {
    nodes {
      name
      company {
        companyName
      }
    }
  }
}
SELECT * FROM companies WHERE company_id=1;
SELECT * FROM companies WHERE company_id=1;
SELECT * FROM companies WHERE company_id=2;
SELECT * FROM companies WHERE company_id=3;
-- ・・・

作为解决这个”N+1问题”的方法之一,我们可以使用dataloader。它的作用是不要每次都向后端发送请求,而是将一段时间内的处理结果积累起来,然后再一并发送到后端。通过这种方式,我们可以将下面的查询合并成一个。

SELECT * FROM companies WHERE company_id IN (1,2,3);

引入·实施

为了实现这个,我们将使用 dataloaden。

$ go get github.com/vektah/dataloaden

Go语言的dataloader通过代码生成来实现,而其他语言的dataloader似乎是通过泛型等方式来实现的。

$ mkdir dataloader
$ cd dataloader
$ echo "package dataloader" > gen.go
$ go run github.com/vektah/dataloaden CompanyLoader string "*github.com/hiroyky/go_graphql_server_sample/graph/model.Company"

dataloader/companyloader_gen.go文件已生成。在获取公司信息时,我们尝试编辑以使用此CompanyLoader。

首先,在resolver.go文件中添加一个字段,以便各个解析器都能够引用它。

type Resolver struct{
    CompanyLoader *dataloader.CompanyLoader // 追記
}

在server.go中,在GraphQL服务器启动时生成CompanyLoader并传递。(我认为这不应该在main函数中写,但为了简化起见,我将其写在了main函数中。)

我将编写一个从Fetch中获取数据的函数。参数keys会在调用时按顺序传递给该函数,并返回按照keys的顺序的值。

func main() {
// ・・・省略・・・
    companyLoader := dataloader.NewCompanyLoader(dataloader.CompanyLoaderConfig{
        MaxBatch: 100, // 溜める最大数、0を指定すると制限無し
        Wait:     2 * time.Millisecond, // 溜める時間
        Fetch: func(keys []string) ([]*model.Company, []error) {
            companies := make([]*model.Company, len(keys))
            errors := make([]error, len(keys))

            // 取得処理を書く SELECT * FROM company WHERE company_id IN (...)

            // 引数のkeysに対応する順番の配列で返す。
            return companies, errors
        },
    })

    srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{
        CompanyLoader: companyLoader,
    }}))
// ・・・省略・・・
}

你可以通过调用Company.Load来执行之前在Fetch中定义的函数。它不会立即执行,而是会将函数调用积累起来并一起执行。

func (r *employeeResolver) Company(ctx context.Context, obj *model.Employee) (*model.Company, error) {
    return r.CompanyLoader.Load(obj.CompanyID)
}

重新总结关于DataLoader的生成命令。

$ cd dataloader
$ echo "package dataloader" > gen.go

■int型を引数に取る関数でロードしたい場合
先ほどはコマンドの第二引数に`string`を指定しました。これは`Fetch関数`の引数の型に対応します。従って`CompanyLoader.Load()`関数の引数を`int`型にしたい場合は`int`を指定します。
$ go run github.com/vektah/dataloaden CompanyLoader int "*github.com/hiroyky/go_graphql_server_sample/graph/model.Company"

■配列を戻り値のロード関数を生成したい場合
`Load()`関数で配列を戻り値にすることもできます。`Fetch`関数の配列処理の実装が少し複雑になるので注意してください。おそらく二重のforループが発生するため。
$ go run github.com/vektah/dataloaden CompaniesLoader int "[]*github.com/hiroyky/go_graphql_server_sample/graph/model.Company"

為了解決以上N+1問題,我們引入了datalodaer。我已經整理成拉取請求。分支名稱是feat5。您可以在這個鏈接中找到:https://github.com/hiroyky/go_graphql_server_sample/pull/5

公式文献:
– gqlgen官方文档:https://gqlgen.com/reference/dataloaders/
– vektah/dataloaden GitHub仓库:https://github.com/vektah/dataloaden

查询的重量限制

简介/概述/概括

在GraphQL的API中,客户端可以自由地创建和请求查询。然而,一个问题是客户端可以轻松地请求到负载较重的查询。因此,在执行查询之前,有一个功能可以计算查询的重量,如果超过一定阈值,则不执行查询,而是返回错误响应。

我认为在将GraphQL的API全局公开时,这个功能是必需的。但如果访问仅限于内部服务器等情况,也可以通过与开发访问源系统的团队事先商讨查询内容来解决。

引入·实施

将以下一行代码加入到 server.go 文件中即可完成导入。该代码用于指定最大重量的参数。每个项目按1计算,超过该值将返回错误响应。

srv.Use(extension.FixedComplexityLimit(10)) // 重さが10を超えたらエラーにする

然而,并不是所有的项目都具有相同的负载。例如,返回数组的项目或更新系的突变应该比通常的查询更重。因此,我们可以针对模式的每个项目定义计算函数。

编辑服务的配置,该配置以前直接传递给服务器的New函数作为参数。

srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{CompanyLoader: companyLoader}}))

将上述内容按如下方式进行更改:定义一个函数,该函数可以在c.Complexity以下的情况下计算重量。函数的第一个参数是请求中所需响应的项目数量,第二个参数是请求的参数。根据这些参数计算重量,并以数字形式返回。

    c := generated.Config{Resolvers: &graph.Resolver{CompanyLoader: companyLoader}}
    c.Complexity.Mutation.CreateCompany = func(childComplexity int, input model.CreateCompanyInput) int {
        return 5
    }
    srv := handler.NewDefaultServer(generated.NewExecutableSchema(c))

尽管如此,在计算查询重量的处理中,仅限制访问源为内部服务器,并与访问源的开发团队事先协商查询内容,如果有可能的话,也许更好.

概括

我对使用gqlgen在GO语言中实现GraphQL服务器的步骤进行了描述。
使用gqlgen的开发步骤如下所示。这是一个循环的过程。

    模式定义、代码生成、调整生成的代码、代码生成、在解析器中编写处理逻辑

在其他方面,我还编写了错误处理和负载控制的代码。非常感谢您的阅读。您辛苦了。

广告
将在 10 秒后关闭
bannerAds