介绍
gqlgen是一个用于在Go中创建GraphQL应用程序的库。在本文中,我们尝试RESTFul API的进阶版本,采用graphql实现服务器。

Graphql

GraphQL服务器能够接收GraphQL查询语言格式的请求并以所需的形式返回响应。GraphQL是一种API查询语言,可以发送查询并询问您需要什么并准确获取该数据。在这个示例查询中,我们正在寻找地址、链接的标题和添加它的用户的名称:

query { 
    links{ 
        title
        address,
        user{ 
            name
        }
    }
}

响应:

{
  "data": {
    "links": [
      {
        "title": "our dummy link",
        "address": "https://address.org",
        "user": {
          "name": "admin"
        }
      }
    ]
  }
}

模式驱动的开发

在GraphQL中,API从定义所有类型、查询和突变的模式开始,它可以帮助其他人理解API。所以它就像服务器和客户端之间的合同,每当我们需要向GraphQL API添加新功能时,我们必须重新定义模式文件,然后在代码中实现该部分。为此,GraphQL有其模式定义语言。gqlgen是一个用于构建GraphQL服务器的Go库,它有一个很好的特性,可以根据你的模式定义生成代码。

起步

考虑我们在之前文章中描述的页面模型;它非常简单,所以要真正使用GraphQL,我们需要让它更真实一些。让我们为每个页面添加一系列附件;Go模型现在看起来像这样:

type Attachment struct {
   Name     string    `json:"Name"`
   Date     time.Time `json:"Date"`
   Contents string    `json:"Contents"`
}

type Page struct {
   ID          int           `json:"Id"`
   Text        string        `json:"Text"`
   Tags        []string      `json:"Tags"`
   Due         time.Time     `json:"Due"`
   Attachments []*Attachment `json:"Attachments"`
}

具体来说,我们的服务器存在API许多返回页面列表,假设页面现在有如上所述的附件,附件可能相当大。现在使用GET /tag/获取标签的所有任务可能会返回大量数据,然而客户端可能只需要任务的名称,这并不理想,尤其是当网络连接速度较慢或带宽花费较贵时(想想移动客户端,在手机应用程序中向您显示任务列表)。 这就是REST API过度获取的问题。
为了解决过度获取问题,一种常见的方法是只返回带有GET /tag/的页面ID列表,而不是页面本身。然后,有了这个ID列表,客户端可以对其进行迭代并为每个ID发出 GET /page/请求。但是,这里仍然可能有些过度获取,因为我们可能不希望整个页面带有附件;所以我们可以选择只返回附件名称(或ID),并有另一个API来获取这些。
还有获取不足问题,我们发出一大堆网络请求,一些用于获取页面,一些用于获取页面中的附件,在延迟较高的情况下,就会造成严重问题。

graphql如何解决

显然,我们可以设计一个特定的REST API来为我们提供所需的数据。例如,GET /page-name-and-attachment-name-in-tag/可以返回一个页面名称列表,每个页面名称都有一个附件名称列表。没有过度获取,没有获取不足,并且一些REST API具有像这样的专用API;但问题很明显,这很难扩展;现在假设我想要相同的信息,但不是标签,而是截止日期。那么就必须为截止日期编写一个非常相似的API。很明显,随着API变得越来越复杂,将会有很多重复。
另一种选择是我可以向其提交更复杂的请求。我们称之为查询,该查询将准确表达我想要的数据的哪些部分,有点像SQL语言。
这就是GraphQL所做的,让我们看看GraphQL如何解决这个问题。这是一个可以由客户端发送的GraphQL查询:

query {
  getTasksByTag(tag: "shopping") {
    Text
    Attachments{
      Name
    }
  }
}

它将返回一个页面列表,但对于每个页面,只会返回页面的文本及其所有附件的名称,这正是在一个单一的响应中我们想要的数据。

带有GraphQL的Go服务器

在这里,我们将之前实现的服务器版本重写为graphql,数据模型已更新为包括附件。
我们采用开头提到的gqlgen,它以GraphQL模式作为输入,并生成一堆Go代码来实现HTTP服务器,以便在这些模式中提供查询服务;实际的处理程序(在GraphQL用语中称为解析器)作为存根留给开发人员实现。
这是我们的服务器后端的GraphQL模式:

type Query {
    getAllPages: [Page]
    getPage(id: ID!): Page

    getPagesByTag(tag: String!): [Page]
    getPagesByDue(due: Time!): [Page]
}

type Mutation {
    createPage(input: NewPage!): Page!

    deletePage(id: ID!): Boolean
    deleteAllPages: Boolean
}

scalar Time

type Attachment {
    Name: String!
    Date: Time!
    Contents: String!
}

type Page {
    Id: ID!
    Text: String!
    Tags: [String!]
    Due: Time!
    Attachments: [Attachment!]
}

input NewAttachment {
    Name: String!
    Date: Time!
    Contents: String!
}

input NewPage {
    Text: String!
    Tags: [String!]
    Due: Time!
    Attachments: [NewAttachment!]
}

这里注意几点:

  • Query和Mutation类型在GraphQL中是特殊的,它们定义了实际的API。GraphQL是强类型的,可以使更好地验证输入(通常在REST中使用的JSON的强类型要弱得多)。
  • 尽管Query类型的API似乎返回[Page],它是一个页面列表,但这里并没有过度获取。GraphQL允许客户端从查询中的返回值中准确指定他们想要的字段,并且只有这些字段通过网络传输。
  • GraphQL没有时间和日期的内置类型,但可以编写扩展;在这里,我使用的是gqlgen中内置的标量时间扩展,它将它映射到Go的time.Time。

最后,schema重复定义了Task和Attachment的NewTask和NewAttachment类型。GraphQL类型可以表示图,从某种意义上说,一个页面可以有多个附件,但理论上每个附件可以属于多个页面。这可能是GraphQL名称中的“图”的来源,它与我们设计关系数据库的方式非常不同。
下面是我实现的步骤:

  1. go run github.com/99designs/gqlgen init
  2. 编写GraphQL架构,如上所述
  3. go run github.com/99designs/gqlgen generate
  4. 更新生成的代码以实现解析器

对于解析器,gqlgen定义了一个名为Resolver的空结构类型,在其上定义了处理程序方法。此结构应由应用程序更新,以包含所有解析器所需的任何共享上下文信息。

type Resolver struct {
   Store *page.Store
}

gqlgen还生成我们应该填写的存根处理程序方法,对于大多数解析器来说,这是微不足道的,例如:

func (r *queryResolver) GetAllPages(context.Context) ([]*model.Page, error) {
   return r.Store.GetAllPages(), nil
}

最后,我们改写一下main函数:

const port = "8880"

func main() {

   srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: graph.NewResolver()}))

   http.Handle("/", playground.Handler("GraphQL playground", "/query"))
   http.Handle("/query", srv)

   log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
   log.Fatal(http.ListenAndServe(":"+port, nil))
}

这里的handler.NewDefaultServer是GraphQL服务器,它在/query路径上注册。我们可以在这里添加更多路径,甚至可以将REST与GraphQL混合使用。

GraphQL vs. REST

在基本层面上,GraphQL与REST相比具有一些明显的优势,特别是在效率方面,过度或不足会导致REST API不理想。也就是说,GraphQL的这种灵活性是有代价的:提交任意复杂的GraphQL查询变得非常容易,从而导致对服务器的DoS攻击。
GraphQL是一种新兴技术,然而REST已经存在了很长时间,今天几乎所有的服务器都有一个REST API,并且已经构建了很多工具来监控、记录、分析和其他对REST API的内省。 REST也非常简单,只是通过HTTP访问路径,通常可以通过简单的curl调用或在浏览器中执行。GraphQL涉及更多,因为查询必须放在POST请求的正文中。
REST的简单性有更深的含义;在典型的Web后端中,REST查询将由发送到数据库的(通常不重要的)SQL查询提供服务。GraphQL添加了自己的查询语言,它在某些方面有点像 SQL,但在其他方面也有很大不同,因为它所依赖的图模型并不是真正的关系型。
还有一点值得一提的是缓存。REST与HTTP缓存配合得很好,因为它大部分依赖于幂等GET请求。GraphQL在这方面比较棘手,因为它不区分HTTP级别的幂等数据查询和突变。

Logo

一起探索未来云端世界的核心,云原生技术专区带您领略创新、高效和可扩展的云计算解决方案,引领您在数字化时代的成功之路。

更多推荐