【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
我已在这里部署了此内容。
https://github.com/hiroyky/go_graphql_server_sample/pull/1
GraphQL服务器开发的流程
刚才的自动生成中,以下3个文件也已经实施,内容如下:
schema.graphqlsにTODOリストを模したスキーマが定義されています。
model/model_gen.goにGo言語でその型定義が記述されています。
schema.resolvers.goに処理を実装するための雛形が記載されています。
创建程序的步骤如下:
-
- 1. 在.graphqls文件中编写GraphQL的模式定义。
-
- 执行gqlgen自动生成命令(gqlgen)。
-
- 检查已创建/更新的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进行讨论。
删除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在一些情况下也采用基于游标的分页方式,但本次不会使用。
在准备分页定义时,将以下内容添加到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查询。我认为这是一个可能引起困惑的地方,所以让我们一起试一试吧。
将自动生成的模型类型更改。由于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的开发步骤如下所示。这是一个循环的过程。
- 模式定义、代码生成、调整生成的代码、代码生成、在解析器中编写处理逻辑
在其他方面,我还编写了错误处理和负载控制的代码。非常感谢您的阅读。您辛苦了。