https://blog.csdn.net/qq_41882147/article/details/82966783
一种用于 API 的查询语言。
ask exactly what you want.
二、为什么要使用GraphQL?
在实际工作中往往会有这种情景出现:比如说我需要展示一个游戏名的列表,可接口却会把游戏的详细玩法,更新时间,创建者等各种各样的 (无用的) 信息都一同返回。
问了后端,原因大概如下:
原来是为了兼容PC端和移动端用同一套接口
或者在整个页面,这里需要显示游戏的标题,可是别的地方需要显示游戏玩法啊,避免多次请求我就全部返回咯
或者是因为有时候项目经理想要显示“标题+更新时间”,有时候想要点击标题展开游戏玩法等等需求,所以把游戏相关的信息都一同返回
简单说就是:
兼容多平台导致字段冗余
一个页面需要多次调用 API 聚合数据
需求经常改动导致接口很难为单一接口精简逻辑
有同学可能会说那也不一定要用GraphQL啊,比方说第一个问题,不同平台不同接口不就好了嘛
http://api.xxx.com/web/getGameInfo/:gameID
http://api.xxx.com/app/getGameInfo/:gameID
http://api.xxx.com/mobile/getGameInfo/:gameID
或者加个参数也行
http://api.xxx.com/getGameInfo/:gameID?platfrom=web
1
这样处理的确可以解决问题,但是无疑加大了后端的处理逻辑。你真的不怕后端程序员打你?
这个时候我们会想,接口能不能不写死,把静态变成动态?
GraphQL是Facebook2015年开源的数据查询规范。现今的绝大多数Web Service都是RESTful的,也就是说,client和server的主要沟通模式还是靠client根据自己的需要向server的若干个endpoint (url)发起请求。由于功能的日渐丰富,对Web Application的要求变得复杂,REST的一些问题逐渐暴露,人们开始思考如何应对这些问题。GraphQL便是具有代表性的一种。GraphQL这个名字,Graph + Query Language,就表明了它的设计初衷是想要用类似图的方式表示数据:即不像在REST中,数据被各个API endpoint所分割,而是有关联和层次结构的被组织在一起。
比方说,假设这么一个提供user信息的REST API:
1 GET
{
“user”: {
“id” : “u3k2k3k178”,
“name” : “graph_ql_activist”,
“email” : “graph_ql@activist.com”,
“avatar” : “img-url”
}
}
2 GET
3 GET
然而如果使用GraphQL,一次API请求即可获取所有信息并且只选取需要的信息(比如关于用户只需要name不要email, followers只要最前面的5个name,followed-users只要头像等等):
query {
user (id : “u3k2k3k178”) {
name
followers (first: 5) {
name
}
followed-users {
avatar
}
}
}
我们会得到一个完全按照query定制的,不多不少的返回结果(一般是一个json对象)。
5个使用GraphQL的理由
使用GraphQL的理由, 必然是从讨论RESTful Service的局限性和问题开始。
数据冗余和请求冗余 (overfetching & underfetching)
灵活而强类型的schema
接口校验 (validation)
接口变动,维护与文档
开发效率
1 数据冗余和请求冗余 (overfetching & underfetching)
根据users API的例子,我们可以想见,GET用户信息的REST call,我们就算只是想要一个用户的一两条信息(比如name & avatar),通过该API,我们也会得到他的整个信息。所谓的overfetching就是指的这种情况——请求包含当前不需要的信息。这种浪费会一定程度地整体影响performance,毕竟更多的信息会占用带宽和占用资源来处理。
同样从上面的例子我们可以看出来,在许多情况下,如果我们使用RESTful Application,我们常常会需要为联系紧密并总量不大的信息,对server进行多次请求,call复数个API。
举一个例子,获取ID为”abc1”和”abc2”的两个用户的信息,我们可能都需要两个API call,一百个用户就是一百个GET call,这是不是很莫名其妙呢?这种情况其实就是underfetching——API的response没有合理的包含足够信息。
然而在GraphQL,我们只需要非常简单地改变schema的处理方式,就可以用一个GET call解决:
query {
user (ids : [“ab1”, “abc2”, …])
}
我们新打开一个网页,如果是RESTful Application,可能请求数据就会马上有成百上千的HTTP Request,然而GraphQL的Application则可能只需要一两个,这相当于把复杂性和heavy lifting交给了server端和cache层,而不是资源有限,并且speed-sensitive的client端。
2 灵活而强类型的schema
GraphQL是强类型的。也就是说,我们在定义schema时,类似于使用SQL,是显式地为每一个域定义类型的,比如说:
type User {
id: ID!
name: String!
joinedAt: DateTime!
profileViews: Int! @default(value: 0)
}
type Query {
user(id: ID!): User
}
GraphQL的schema的写作语言,其实还有一个专门的名称——Schema Definition Language (SDL)。
这件事情的一大好处是,在编译或者说build这个Application时,我们就可以检查并应对很多mis-typed的问题,而不需要等到runtime。同时,这样的写作方式,也为开发者提供了巨大的便利。比如说使用YAML来定义API时,编写本身就是十分麻烦的——可能没有理想的auto-complete,语法或者语义有错无法及时发现,文档也需要自己小心翼翼地编写。就算有许多工具(比如Swagger)帮助,这仍然是一个很令人头疼的问题。
3 接口校验 (validation)
显而易见,由于强类型的使用,我们对收到的数据进行检验的操作变得更为容易和严格,自动化的简便度和有效性也大大提高。对query本身的结构的校验也相当于是在schema完成后就自动得到了,所以我们甚至不需要再引入任何别的工具或者依赖,就可以很方便地解决所有的validation。
4 接口变动,维护与文档
RESTful Application里面,一旦要改动API,不管是增删值域,改变值域范围,还是增减API数量,改变API url,都很容易变成伤筋动骨的行为。
如果说改动API url(比如/posts –> /articles),我们思考一下那些地方可能要改动呢?首先client端的代码定然要改变request的API endpoint;中间的caching service可能也需要改要访问的endpoint;如果有load balancer, reverse proxy,那也可能需要变动;server端自己当然也是需要做相应改变的,这根据application自己的编写情况而定。
相比之下,GraphQL就轻松多了。GraphQL的Service,API endpoint很可能就只有一个,根本不太会有改动URL path的情况。至始至终,数据的请求方都只需要说明自己需要什么内容,而不需要关心后端的任何表述和实现。数据提供方,比如server,只要提供的数据是请求方的母集,不论它们各自怎么变,都不需要因为对方牵一发而动全身。
在现有工具下,REST API的文档没有到过分难以编写和维护的程度,不过跟可以完全auto-generate并且可读性可以很好地保障的GraphQL比起来,还是略显逊色——毕竟GraphQL甚至不需要我们费力地引入多少其他的工具。
再一点,我们都知道REST API有一个versioning: V1, V2, etc.这件事非常的鸡肋而且非常麻烦,有时候还要考虑backward compatibility。GraphQL从本质上不存在这一点,大大减少了冗余。增加数据的fields和types甚至不需要数据请求方做任何改动,只需要按需添加相应queries即可。
另外,有了GraphQL的queries,我们可以非常精准地进行数据分析(Analytics)。比如说具体哪些queries下的fields / objects在哪些情况下是被请求的最多/最频繁的——而不像RESTful Application中,如果不进行复杂的Analytics,我们只能知道每个API被请求的情况,而不是具体到它们内含的数据。
5 开发效率
相信上面说的这些点已经充分能够说明GraphQL对于开发效率能够得到怎样的提升了。
再补充几点。
GraphQL有一个非常好的ecosystem。由于它方便开发者上手和使用–>大家争相为它提供各种工具和支持–>GraphQL变得更好用–>社区文化和支持更盛–>… 如同其他好的开源项目一样,GraphQL有着一个非常好的循环正向反馈。
对于一套REST API,哪怕只是其使用者(consumer),新接触的开发者需要一定时间去熟悉它的大致逻辑,要求乃至实现。然而GraphQL使用者甚至不需要去看类似API文档的东西,因为我们可以直接通过query查询query里面所有层级的type的所有域和它们各自的type,这不得不说很方便:
{
__schema {
types {
name
}
}
}
==> 我们可以看到query所涉及的所有内容的类型:
{
“data”: {
“__schema”: {
“types”: [
{
“name”: “Query”
},
{
“name”: “Episode”
},
{
“name”: “Character”
},
{
“name”: “ID”
},
{
“name”: “String”
},
{
“name”: “Int”
},
{
“name”: “FriendsConnection”
},
{
“name”: “FriendsEdge”
},
{
“name”: “PageInfo”
}
{
“name”: “__Schema”
},
{
“name”: “__Type”
},
{
“name”: “__TypeKind”
},
{
“name”: “__Field”
},
{
“name”: “__InputValue”
},
{
“name”: “__EnumValue”
}
}
]
}
}
}
对于GraphQL,我还有个非常个人的理由偏爱它:对于API的测试,相比于比较传统的Postman或者自己写脚本进行最基本的http call(或者curl),我更喜欢使用insomnia这个更为优雅的工具。而在此之上,它还非常好地支持了GraphQL,这让我的开发和测试体验变得更好了。(Postman至今还不支持GraphQL,虽然本质上我们可以用它make GraphQL query call)
5个不用GraphQL的理由
迁移成本
牺牲Performance
缺乏动态类型
简单问题复杂化
缓存能解决很多问题
1 使用与迁移成本
现有的RESTful Application如果要改造成GraphQL Application?
hmmm…
我们需要三思。首先我就不说RESTful本来从end to end都有成熟高效解决方案这样的废话了。迁移的主要问题在于,它从根本上改变了我们组织并暴露数据的方式,也就是说对于application本身,从数据层到业务逻辑层,可能有极其巨大的影响。所以它非常不适合现有的复杂系统“先破后立”。一个跑着SpringMVC的庞大Web Application如果要改成时髦的GraphQL应用?这个成本和破坏性难以预计。
并且,尽管我们说GraphQL有着很好的社区支持,但本质上使用GraphQL,就等于要使用React与NodeJS。所以如果并不是正在使用或者计划使用React和Node,GraphQL是不适合的。
2 牺牲Performance
Performance这件事是无数人所抱怨的。如同我们前面所说的,GraphQL的解决方案,相当于把复杂性和heavy lifting从用户的眼前,移到了后端——很多时候,就是数据库。
要讨论这一点,我们首先要提的是,为了支持GraphQL queries对于数据的查询,开发者需要编写resolvers。
比如说这样一个schema:
type Query {
human(id: ID!): Human
}
type Human {
name: String
appearsIn: [Episode]
starships: [Starship]
}
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
type Starship {
name: String
}
对于human,我们就需要一个最基础的resolver:
Query: {
human(obj, args, context, info) {
return context.db.loadHumanByID(args.id).then(
userData => new Human(userData)
)
}
}
当然这还没完,对不同的请求类型,我们要写不同的resolver——不仅原来REST API的CRUD我们都要照顾到,可能还要根据业务需求写更多的resolver。
这件事情造成的影响,除了开发者要写大量boilerplate code以外,还可能导致查询性能低下。一个RESTful Application,由于每个API的确定性,我们可以针对每一个API的逻辑,非常好的优化它们的性能,所以就算存在一定程度的overfetching/underfetching,前后端的性能都可以保持在能够接受的范围内。然而想要更普适性一些的GraphQL,则可能会因为一个层级结构复杂而且许多域都有很大数据量的query跑许多个resolvers,使得数据库的查询性能成为了瓶颈。
3 缺乏动态类型
强类型的schema固然很省力,但是如果我们有时候想要一些自由(flexibility)呢?
比方说,有时候请求数据时,请求方并不打算定义好需要的所有层级结构和类型与域。比方说,我们想要单纯地打印一些数据,或者获取一个user的一部分fields直接使用,剩下部分保存起来之后可能使用可能不使用,但并不确定也不关心剩下的部分具体有那些fields——多余的部分可能作为additional info,有些域如果有则使用,没有则跳过。
这只是一个例子,但是并不是一个钻牛角尖的例子——因为有时候我们所要的objects的properties本来就可能是dynamic的,我们甚至可能会通过它的properties/fields来判定它是一个怎样的object。
我们要怎么处理这种问题呢?一种有些荒诞现实主义的做法是,往Type里加一个JSON string field,用来提供其相关的所有信息,这样就可以应对这种情况了。但是这是不是一个合理的做法呢?
4 简单问题复杂化
最显著的例子,就是error handling。REST API的情况下,我们不需要解析Response的内容,只需要看HTTP status code和message,就能知道请求是否成功,大概问题是什么,处理错误的程序也十分容易编写。
然而GraphQL的情景下,hmmm…
只要Service本身还在正常运行,我们就会得到200的HTTP status,然后需要专门检查response的内容才知道是否有error:
{
“errors”: [
{
“message”: “Field "name" must not have a selection since type "String" has no subfields.”,
“locations”: [
{
“line”: 31,
“column”: 101
}
]
}
]
}
Another layer of complexity.
同时,简单的Application,使用GraphQL其实是非常麻烦的——比如前面提到的resolvers,需要大量的boilerplate code。另外,还有各种各样的Types, Queries, Mutators, High-order components需要写。相比之下,反倒是REST API更好编写和维护。
5 缓存能解决很多问题
编写过HTTP相关程序之后应该都知道,HTTP本身就是涵盖caching的,更不要提人们为了提高RESTful Application的performance而针对缓存作出的种种努力。
对于overfetching和请求次数冗余的问题,假设我们的整个application做了足够合理的设计,并且由于REST API的固定和单纯性,缓存已经能非常好地减少大量的traffic。
然而如果选择使用GraphQL,我们就没有了那么直白的caching解决方案。首先,只有一个API endpoint的情况下,每个query都可能不同,我们不可能非常轻松地对request分门别类做caching。当然并不是说真的没有现成的工具,比如说Appollo client就提供了InMemoryCache并且,不论有多少queries,总是有hot queries和cold ones,那么pattern总是有的。针对一些特定的query我们还可以定向地缓存,比如说PersistGraphQL便是这样一个工具。然而这样做其实又是相当于从queries中提炼出类似于原来的REST API的部分了,并且又增加了一层complexity,不管是对于开发还是对于performance,这都可能有不容忽视的影响。
总结
GraphQL最大的优势,就是它能够大大提高开发者的效率,而且最大化地简化了前端的数据层的复杂性,并且使得前后端对数据的组织观点一致。只是使用时,需要考察scale, performance, tech stack, migration等等方面的要求,做合理的trade-off,否则它可能不仅没能提高开发者效率,反倒制造出更多的问题。
References
Execution
Mutations
Introduction to GraphQL
graphql/releases/tag/June2018
Mocking your server is easy with GraphQL
https://www.jianshu.com/p/12dff5905cf6
https://www.jianshu.com/p/4f33f36bb034
GraphQL是比REST更高效、强大和灵活的新一代API标准。Facebook开发了GraphQL并且将其开源,目前其由一大群来自全球各地的公司和个人维护。
注意到GraphQL是API标准,不要看到QL结尾就以为其是一种数据库技术。
比REST更灵活的一种选择
REST是目前比较流行的一种暴露服务端数据的常见方式,其简化了客户端尤其是移动端和服务器交互的流程。但是随着业务变得复杂,有些情况变得棘手:
移动端数量的增多,对数据的效率要求变高
移动端和PC端相比,是需要提高对数据获取的效率的,这个效率就是说要减少网络请求、要减少无用数据的传输。
应对复杂的前端框架和平台
现在的情况是仅维护一套API来应对不同框架和平台的请求。PC端一个页面比移动端一个页面展示的内容要多很多,之前后端提供给PC端的API如果直接提供给移动端来使用势必造成资源浪费。所以移动端的人会去找后端的人干一架,结果要么是后端再给移动端单独写一套API,要么就是移动端忍受着API请求返回数据中存在大量冗余的数据。
需要更快速地迭代更新
互联网时代最大的特色除了加班也许就是快了。好多公司在喊着小步快跑、快速试错,毕竟市场不等人。然而REST标准的API似乎很难快速地跟上这快跑的节奏。也许一个API刚出来,产品那边已经改了原型,界面重新设计了。这时候就要麻烦后端同学加个班把接口改一下吧。
谁在用GraphQL
一个产品的流行,肯定是解决了目前的某些痛点。虽然GraqhQL目前在国内还不算流行,可是在美利坚已经有不少巨头在使用了:
image.png
GraphQL vs REST
我们来看一下对于不同API标准下,从服务端获取数据的区别。比如在REST API标准下,有三个接口:
/users/
该接口返回某用户基本信息
/users/posts
该接口返回某用户所有的文章
/users/followers
该接口返回某用户所有的关注者
image.png
如图所示,要通过三个不同的请求才能获得某用户及其文章和关注者的信息,其中还存在很多不需要的信息。
再看一下GraphQL API的实现:
image.png
客户端声明自己想要的信息,然后服务端根据请求返回相应的数据
目前可见的优点:
避免了REST API中常见的信息过多或过少的问题
信息过多是指,接口中总会存在客户端不需要的信息,信息过少是指单条接口无法满足客户端需求,需要请求多个接口才能满足需要
前端可以快速迭代
在REST API中,一般都是后端定义好了API,返回固定的数据格式。当前端业务或需求发生变化时,后端很难跟上变动的节奏。如今,业务变化已经难以避免,所以当前端和后端都要相应地作出改动,这样效率势必降低。就我们公司业务来讲,很多情况下,前端一两天的改动如果再拉上后端,人多肯定要开会再加上沟通成本的问题,这个需求没个一周两周很难搞定。设想一下,如果在GraphQL标准下,除非大的改版,后端基本不用出人力来跟着一起需求评审,前端自己定义查询的内容就搞定了。
更深层次地进行分析
当客户端可以选择自己想请求数据的内容时,这时候就可以分析出哪些信息是用户感兴趣的,也可以更深层次地分析现有数据是如何被应用的。
此外,也可以分析出哪些信息用户不再感兴趣了。以上转自腾讯云GraphQL简介
在GraphQL中,我们通过预先定义一张Schema和声明一些Type来达到上面提及的效果,我们需要知道
对于数据模型的抽象是通过Type来描述的
对于接口获取数据的逻辑是通过Schema来描述的
Type
对于数据模型的抽象是通过Type来描述的,每一个Type有若干Field组成,每个Field又分别指向某个Type。
GraphQL的Type简单可以分为两种,一种叫做Scalar Type(标量类型),另一种叫做Object Type(对象类型)。
Scalar Type
GraphQL中的内建的标量包含,String、Int、Float、Boolean、Enum,对于熟悉编程语言的人来说,这些都应该很好理解。
值得注意的是,GraphQL中可以通过Scalar声明一个新的标量,比如:
prisma(一个使用GraphQL来抽象数据库操作的库)中,还有DateTime和ID这两个标量分别代表日期格式和主键
在使用GraphQL实现文件上传接口时,需要声明一个Upload标量来代表要上传的文件
总之,我们只需要记住,标量是GraphQL类型系统中最小的颗粒,关于它在GraphQL解析查询结果时,我们还会再提及它。
Object Type
仅有标量是不够的抽象一些复杂的数据模型的,这时候我们需要使用对象类型
type Article {
id: ID
text: String
isPublished: Boolean
}
上面的代码,就声明了一个Article类型,它有3个Field,分别是ID类型的id,String类型的text和Boolean类型的isPublished。
对于对象类型的Field的声明,我们一般使用标量,但是我们也可以使用另外一个对象类型,比如如果我们再声明一个新的User类型,如下:
type Article {
id: ID
text: String
isPublished: Boolean
author: User
}
Article新增的author的Field是User类型, 代表这篇文章的作者。
总之,我们通过对象模型来构建GraphQL中关于一个数据模型的形状,同时还可以声明各个模型之间的内在关联(一对多、一对一或多对多)。
mongoose
image.png
Schema: Mongoose 的一切始于 Schema。每个 schema 都会映射到一个 MongoDB collection ,并定义这个collection里的文档的构成。
定义一个schema
const mongoose = require(‘mongoose’);
const Schema = mongoose.Schema;
let blogSchema = new Schema({
title: String,
author: String,
body: String,
comments: [{ body: String, date: Date }],
date: { type: Date, default: Date.now },
hidden: Boolean,
meta: {
votes: Number,
favs: Number
}
});
Model: 基本文档数据的父类,通过集成Schema定义的基本方法和属性得到相关的内容.
Models 是从 Schema 编译来的构造函数。 它们的实例就代表着可以从数据库保存和读取的 documents。 从数据库创建和读取 document 的所有操作都是通过 model 进行的。
var schema = new mongoose.Schema({ name: ‘string’, size: ‘string’ });
var Tank = mongoose.model(‘Tank’, schema);
第一个参数是跟 model 对应的集合( collection )名字的 单数 形式。 Mongoose 会自动找到名称是 model 名字 复数 形式的 collection 。 对于上例,Tank 这个 model 就对应数据库中 tanks 这个 collection。.model() 这个函数是对 schema 做了拷贝(生成了 model)。 你要确保在调用 .model() 之前把所有需要的东西都加进 schema 里了!
instance: 这就是实实在在的数据了. 通过 new Model()初始化得到.
https://www.jianshu.com/p/8971ae679201
https://graphql.org.cn/
https://graphql.org.cn/code.html#go
https://github.com/graphql-go/graphql
https://blog.csdn.net/phantom_111/article/details/79932759
GraphQL背景
REST API的使用方式是,server定义一系列的接口,client调用自己需要的接口,获取目标数据进行整合。REST API开发中遇到的问题:
扩展性 ,随着API的不断发展,REST API的接口会变得越来臃肿。
无法按需获取 ,一个返回id, name, age, city, addr, email的接口,如果仅获取部分信息,如name, age,却必须返回接口的全部信息,然后从中提取自己需要的。坏处不仅会增加网络传输量,并且不便于client处理数据
一个请求无法获取所需全部资源 ,例如client需要显示一篇文章的内容,同时要显示评论,作者信息,那么就需要调用文章、评论、用户的接口。坏处造成服务的的维护困难,以及响应时间变长 。
原因: REST API通常由多个端点组成,每个端点代表一种资源。所以,当client需要多个资源是,它需要向REST API发起多个请求,才能获取到所需要的数据。
REST API不好处理的问题 , 比如确保client提供的参数是类型安全的,如何从代码生成API的文档等。
GraphQL解决的问题:
请求你的数据不多不少 :GraphQL查询总是能准确获得你想要的数据,不多不少,所以返回的结果是可预测的。
获取多个资源只用一个请求 :GraphQL查询不仅能够获得资源的属性,还能沿着资源间进一步查询,所以GraphQL可以通过一次请求就获取你应用所需的所有数据。
描述所有的可能类型系统: GraphQL API基于类型和字段的方式进行组成,使用类型来保证应用只请求可能的类型,同时提供了清晰的辅助性错误信息。
使用你现有的数据和代码: GraphQL让你的整个应用共享一套API,通过GraphQL API能够更好的利用你的现有数据和代码。GraphQL 引擎已经有多种语言实现,GraphQL不限于某一特定数据库,可以使用已经存在的数据、代码、甚至可以连接第三方的APIs。
API 演进无需划分版本: 给GraphQL API添加字段和类型而无需影响现有查询。老旧字段可以废弃,从工具中隐藏。
什么是GraphQL
GraphQL官网给出定义:GraphQL既是一种用于API的查询语言 也是一个满足你数据查询的运行时 。GraphQL对你的API中的数据提供了一套易于理解的完整描述 ,使得客户端能够准确地获得它需要的数据 ,而且没有任何冗余,也让API更容易地随着时间推移而演进,还能用于构建强大的开发者工具。
API不是用来调用的吗?是的,者正是GraphQL的强大之处,引用官方文档的一句话ask exactly what you want
本质上来说GraphQL是一种查询语言
上述的定义其实很难理解,只有真的使用过GraphQL才能够理解。
在GraphQL中,通过定义一张Schema和声明一些Type来达到上述描述的功能,需要学习:
对于数据模型的抽象是通过Type来描述的 ,如何定义Type?
对于接口获取数据的逻辑是通过schema来描述的 ,如何定义schema?
如何定义Type
对于数据模型的抽象是通过Type来描述的,每一个Type有若干Field组成,每个Field又分别指向某个Type。
GraphQL的Type简单可以分为两种,一种是scalar type(标量类型) ,另一种是object type(对象类型)。
scalar type
GraphQL中的内建的标量包含,String、Int、Float、Boolean、Enum,除此之外,GraphQL中可以通过scalar声明一个新的标量 ,比如:
prisma ——一个使用GraphQL来抽象数据库操作的库中,还有DataTime(日期格式)和主键(ID)。
在使用GraphQL实现文件上传接口时,需要声明一个Upload标量来代表要上传的文件。
标量是GraphQL类型系统中最小的颗粒。
object type
仅有标量是不够抽象一些复杂的数据模型,这时需要使用对象类型。通过对象类型来构建GraphQL中关于一个数据模型的形状,同时还可以声明各个模型之间的内在关联(一对多,一对一或多对多)。
一对一模型
type Article {
id: ID
text: String
isPublished: Boolean
author: User
}
1
2
3
4
5
6
上述代码,声明了一个Article类型,它有3个Field,分别是id(ID类型)、text(String类型)、isPublished(Boolean类型)以及author(新建的对象类型User),User类型的声明如下:
type User {
id: ID
name: String
}
1
2
3
4
lType Modifier
类型修饰符,当前的类型修饰符有两种,分别是List和Required ,语法分别为[Type]和[Type!],两者可以组合:
[Type]! :列表本身为必填项,但内部元素可以为空
[Type!] :列表本身可以为空,但是其内部元素为必填
[Type!]! :列表本身和内部元素均为必填
如何定义Schema
schema用来描述对于接口获取数据逻辑 ,GraphQL中使用Query来抽象数据的查询逻辑,分为三种,分别是query(查询)、mutation(更改)、subscription(订阅) 。API的接口概括起来有CRUD(创建、获取、更改、删除)四类,query可以覆盖R(获取)的功能,mutation可以覆盖(CUD创建、更改、删除)的功能。
注意: Query特指GraphQL中的查询(包含三种类型),query指GraphQL中的查询类型(仅指查询类型)。
Query
query(查询):当获取数据时,选择query类型
mutation(更改): 当尝试修改数据时,选择mutation类型
subscription(订阅):当希望数据更改时,可以进行消息推送,使用subscription类型(针对当前的日趋流行的real-time应用提出的)。
以Article为数据模型,分别以REST和GraphQL的角度,编写CURD的接口
Rest接口
GET /api/v1/articles/
GET /api/v1/article/:id/
POST /api/v1/article/
DELETE /api/v1/article/:id/
PATCH /api/v1/article/:id/
GraphQL Query
query类型
query {
articles():[Article!]!
article(id: Int!): Article!
}
mutation类型
mutation {
createArticle(): Article!
updateArticle(id: Int): Article!
deleteArticle(id: Int): Article!
}
注意:
GraphQL是按照类型来划分职能的query、mutation、ssubscription,同时必须明确声明返回的数据类型。
如果实际应用中对于评论列表有real-time 的需求,该如何处理?
在REST中,可以通过长连接,或者通过提供一些带验证的获取长连接URL的接口,比如POST /api/v1/messages/之后长连接会将新的数据进行实时推送。
在GraphQL中,会以更加声明式的方式进行声明,如下:
subscription {
updatedArticle() {
mutation
node {
comments: [Comment!]!
}
}
}
1
2
3
4
5
6
7
8
此处声明了一个subscription,这个subscription会在有新的Article被创建或者更新时,推送新的数据对象。实际上内部仍然是建立于长连接之上 。
Resolve
上述的描述并未说明如何返回相关操作(query、mutation、subscription)的数据逻辑。所有此处引入一个更核心的概念Resolve(解析函数)
GraphQL中,默认有这样的约定,Query(包括query、mutation、subscription)和与之对应的Resolve是同名的,比如关于articles(): [Articles!]!这个query,它的Resolve的名字必然叫做articles
以已经声明的articles的query为例,解释下GraphQL的内部工作机制:
Query {
articles {
id
author {
name
}
comments {
id
desc
author
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
按照如下步骤进行解析:
首先进行第一次解析,当前的类型是query 类型,同时Resolver的名字为articles。
之后会尝试使用articles的Resolver获取解析数据,第一层解析完毕
之后对第一层解析的返回值,进行第二层解析,当前articles包含三个子query ,分别是id、author和comments
id在Author类型中为标量类型,解析结束
author在articles类型中为对象类型User,尝试使用User的Resolver获取数据,当前field解析完毕。
之后对第二层解析的返回值,进行第三层解析,当前author还包含一个query,name是标量类型,解析结束
comments解析同上
概括总结GraphQL大体解析流程就是遇见一个Query之后,尝试使用它的Resolver取值,之后再对返回值进行解析,这个过程是递归的,直到所有解析Field类型是Scalar Type(标量类型)为止。整个解析过程可以想象为一个很长的Resolver Chain(解析链)。
Resolver本身的声明在各个语言中是不同的,它代表数据获取的具体逻辑。它的函数签名(以golang为例):
func(p graphql.ResolveParams) (interface{}, error) {}
// ResolveParams Params for FieldResolveFn()
type ResolveParams struct {
// Source is the source value
Source interface{}
// Args is a map of arguments for current GraphQL request
Args map[string]interface{}
// Info is a collection of information about the current execution state.
Info ResolveInfo
// Context argument is a context value that is provided to every resolve function within an execution.
// It is commonly
// used to represent an authenticated user, or request-specific caches.
Context context.Context } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 值得注意的是,Resolver内部实现对于GraphQL完全是黑盒状态。这意味着Resolver如何返回数据、返回什么样的数据、从哪里返回数据,完全取决于Resolver本身。GraphQL在实际使用中常常作为中间层来使用,**数据的获取通过Resolver来封装,内部数据获取的实现可能基于RPC、REST、WS、SQL等多种不同的方式。
GraphQL例子
下面这部分将会展示一个用graphql-go实现的用户管理的例子,包括获取全部用户信息、获取指定用户信息、修改用户名称、删除用户的功能,以及如何创建枚举类型的功能,完整代码在这里。
生成后的schema文件内容如下:
type Mutation {
“”“[用户管理] 修改用户名称”””
changeUserName(
“"”用户ID”””
userId: Int!
"""用户名称"""
userName: String! ): Boolean
””“[用户管理] 创建用户”””
createUser(
“"”用户名称”””
userName: String!
"""用户邮箱"""
email: String!
"""用户密码"""
pwd: String!
"""用户联系方式"""
phone: Int ): Boolean
””“[用户管理] 删除用户”””
deleteUser(
“"”用户ID”””
userId: Int!
): Boolean
}
type Query {
“”“[用户管理] 获取指定用户的信息”””
UserInfo(
“"”用户ID”””
userId: Int!
): userInfo
””“[用户管理] 获取全部用户的信息”””
UserListInfo: [userInfo]!
}
”"”用户信息描述”””
type userInfo {
“"”用户email”””
email: String
”"”用户名称”””
name: String
”"”用户手机号”””
phone: Int
”"”用户密码”””
pwd: String
”"”用户状态”””
status: UserStatusEnum
”"”用户ID”””
userID: Int
}
”"”用户状态信息”””
enum UserStatusEnum {
“"”用户可用”””
EnableUser
”"”用户不可用”””
DisableUser
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
注意
GraphQL基于golang实现的例子比较少
GraphQL的schema可以自动生成,具体操作可查看graphq-cli文档,步骤大致包括npm包的安装、graphql-cli工具的安装,配置文件的更改(此处需要指定服务对外暴露的地址) ,执行graphql get-schema 命令。
GraphQL API以及Rsolve函数定义
type UserInfo struct {
UserID uint64 json:"userID"
Name string json:"name"
Email string json:"email"
Phone int64 json:"phone"
Pwd string json:"pwd"
Status model.UserStatusType json:"status"
}
//这段内容是如何使用GraphQL定义枚举类型
var UserStatusEnumType = graphql.NewEnum(graphql.EnumConfig{
Name: “UserStatusEnum”,
Description: “用户状态信息”,
Values: graphql.EnumValueConfigMap{
“EnableUser”: &graphql.EnumValueConfig{
Value: model.EnableStatus,
Description: “用户可用”,
},
“DisableUser”: &graphql.EnumValueConfig{
Value: model.DisableStatus,
Description: “用户不可用”,
},
},
})
var UserInfoType = graphql.NewObject(graphql.ObjectConfig{
Name: “userInfo”,
Description: “用户信息描述”,
Fields: graphql.Fields{
“userID”: &graphql.Field{
Description: “用户ID”,
Type: graphql.Int,
},
“name”: &graphql.Field{
Description: “用户名称”,
Type: graphql.String,
},
“email”: &graphql.Field{
Description: “用户email”,
Type: graphql.String,
},
“phone”: &graphql.Field{
Description: “用户手机号”,
Type: graphql.Int,
},
“pwd”: &graphql.Field{
Description: “用户密码”,
Type: graphql.String,
},
“status”: &graphql.Field{
Description: “用户状态”,
Type: UserStatusEnumType,
},
},
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
query与mutation的定义
var MutationType = graphql.NewObject(graphql.ObjectConfig{
Name: “Mutation”,
Fields: graphql.Fields{
“createUser”: &graphql.Field{
Type: graphql.Boolean,
Description: “[用户管理] 创建用户”,
Args: graphql.FieldConfigArgument{
“userName”: &graphql.ArgumentConfig{
Description: “用户名称”,
Type: graphql.NewNonNull(graphql.String),
},
“email”: &graphql.ArgumentConfig{
Description: “用户邮箱”,
Type: graphql.NewNonNull(graphql.String),
},
“pwd”: &graphql.ArgumentConfig{
Description: “用户密码”,
Type: graphql.NewNonNull(graphql.String),
},
“phone”: &graphql.ArgumentConfig{
Description: “用户联系方式”,
Type: graphql.Int,
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
userId, _ := strconv.Atoi(GenerateID())
user := &model.User{
//展示如何解析传入的参数
Name: p.Args[“userName”].(string),
Email: sql.NullString{
String: p.Args[“email”].(string),
Valid: true,
},
Pwd: p.Args[“pwd”].(string),
Phone: int64(p.Args[“phone”].(int)),
UserID: uint64(userId),
Status: int64(model.EnableStatus),
}
if err := model.InsertUser(user); err != nil {
log.WithError(err).Error(“[mutaition.createUser] invoke InserUser() failed”)
return false, err
}
return true, nil
},
},
}, })
var QueryType = graphql.NewObject(graphql.ObjectConfig{
Name: “Query”,
Fields: graphql.Fields{
“UserListInfo”: &graphql.Field{
Description: “[用户管理] 获取指定用户的信息”,
//定义了非空的list类型
Type: graphql.NewNonNull(graphql.NewList(UserInfoType)),
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
users, err := model.GetUsers()
if err != nil {
log.WithError(err).Error(“[query.UserInfo] invoke InserUser() failed”)
return false, err
}
usersList := make([]*UserInfo, 0)
for _, v := range users {
userInfo := new(UserInfo)
userInfo.Name = v.Name
userInfo.Email = v.Email.String
userInfo.Phone = v.Phone
userInfo.Pwd = v.Pwd
userInfo.Status = model.UserStatusType(v.Status)
usersList = append(usersList, userInfo)
}
return usersList, nil
},
},
}, }) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 注意:
此处仅展示了部分例子
此处笔者仅列举了query、mutation类型的定义
如何定义服务main函数
type ServerCfg struct {
Addr string
MysqlAddr string
}
func main() {
//load config info
m := multiconfig.NewWithPath(“config.toml”)
svrCfg := new(ServerCfg)
m.MustLoad(svrCfg)
//new graphql schema
schema, err := graphql.NewSchema(
graphql.SchemaConfig{
Query: object.QueryType,
Mutation: object.MutationType,
},
)
if err != nil {
log.WithError(err).Error(“[main] invoke graphql.NewSchema() failed”)
return
}
model.InitSqlxClient(svrCfg.MysqlAddr)
h := handler.New(&handler.Config{
Schema: &schema,
Pretty: true,
GraphiQL: true,
})
http.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
//read user_id from gateway
userIDStr := r.Header.Get("user_id")
if len(userIDStr) > 0 {
userID, err := strconv.Atoi(userIDStr)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
ctx = context.WithValue(ctx, "ContextUserIDKey", userID)
}
h.ContextHandler(ctx, w, r)
})
log.Fatal(http.ListenAndServe(svrCfg.Addr, nil)) } 展示下GraphQL自带的GraphiQL调试工具
笔者初次接触GraphQL,可能很多理解有误,欢迎指出。
参考资料
GraphQL官网中文版
30分钟理解GraphQL核心概念
GitHub为什么开放一套GraphQL版本的API?
GraphQL入门
在GraphQL中建模一个博客索引
https://github.com/graphql-go/graphql
https://zhuanlan.zhihu.com/p/35792985
https://github.com/graphql-go/graphql
Github提供的GraphQL接口非常全面,那么我们该如何搭建出自己的接口呢?好在GraphQL提供了很多语言的解决方案。本文主要阐述如何用go搭建自己的GraphQL服务器。如果了解GraphQL建议先阅读GraphQL — API查询语言 或相关资料。
graphql-go
An implementation of GraphQL in Go. Follows the official reference implementation graphql-js.
一套比较完善的框架,众所周知go的结构体对json非常友好,所以并不需要对数据有特殊的处理,还是很方便的。打开终端输入命令
go get github.com/graphql-go/graphql
Object
在服务端编程中,编写的一切都可以称之为对象(Object)。例如一个商品(goods)的实例可以有商品名(name)、价格(price)、购买链接(url)三个字段。此时商品可以很自然的被称为一个object,查询的语句可以写成:
{
goods{
name
price
url
}
}
如果此时我们要查询商品和文章两种object的信息:
/* query 可以省去 */
query{
goods{
name
}
article{
name
}
}
是否你已经发觉,query像一个大的object,它有goods和article两个字段。除此之外,mutation也是如此:
mutation{
addGoods(input:goodsInput){
name
}
}
这里的addGoods可以看做是一个可以处理参数的对象,也就是某种意义上的函数。
总之,GraphQL服务端的编程就是一个又一个的对象将形成的嵌套结构(schema)组织起来,并对外提供服务。
query&mutation
为了防止低级错误的发生,在当前pkg下新建一个名为query.go(随便起)的文件。
import (
“github.com/graphql-go/graphql”
“errors”
)
定义good object
type Goods struct {
ID string json:"id"
Name string json:"name"
Price float64json:"price"
Url string json:"url"
}
var goodsType = graphql.NewObject(
graphql.ObjectConfig{
Name: “Goods”,
Fields: graphql.Fields{
“id”: &graphql.Field{
Type: graphql.String,
},
“name”: &graphql.Field{
Type: graphql.String,
},
“price”: &graphql.Field{
Type: graphql.Float,
},
“url”: &graphql.Field{
Type: graphql.String,
},
},
},
)
var goodsListType = graphql.NewList(goodsType)
注意:数组相当于新的object类型。
定义query object
var queryType = graphql.NewObject(
graphql.ObjectConfig{
Name: “Query”,
Fields: graphql.Fields{
// 无需处理参数
“goodsList”: &graphql.Field{
Type:goodsListType,
// 处理结构体的回调函数,直接返回处理完成的结构体即可
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
return result, err
},
},
// 参数是id
“goods”: &graphql.Field{
Type: goodsType,
Args: graphql.FieldConfigArgument{
“id”: &graphql.ArgumentConfig{
Type: graphql.String,
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
// 获取参数
idQuery, isOK := p.Args[“id”].(string)
if isOK {
return result, nil
}
err := errors.New(“Field ‘goods’ is missing required arguments: id. “)
return nil, err
},
},
},
},
)
mutation定义基本相同,新建一个名为mutation.go的文件:
定义input object
var goodsInputType = graphql.NewInputObject(
graphql.InputObjectConfig{
Name: “goodsInput”,
Fields: graphql.InputObjectConfigFieldMap{
“name”: &graphql.InputObjectFieldConfig{
Type: graphql.String,
},
“price”: &graphql.InputObjectFieldConfig{
Type: graphql.Float,
},
“url”: &graphql.InputObjectFieldConfig{
Type: graphql.String,
},
},
},
)
定义 mutation object
var mutationType = graphql.NewObject(
graphql.ObjectConfig{
Name: “Mutation”,
Fields: graphql.Fields{
“addGoods”:&graphql.Field{
Type:goodsType,
Args:graphql.FieldConfigArgument{
“input”:&graphql.ArgumentConfig{
Type:goodsInputType,
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
input,isOk := p.Args[“input”].(map[string]string)
if !isOk{
err := errors.New(“Field ‘addGoods’ is missing required arguments: input. “)
return nil,err
}
result := Goods{
Name:input[“name”].(string),
Price:input[“price”].(float64),
Url:input[“url”].(string),
}
// 处理数据
return result,err
},
},
},
},
)
然而,input类型并不能直接转换为struct,而是一个map[string]interface{}类型,还需要进行手动转换。
定义schema
var schema, _ = graphql.NewSchema(
graphql.SchemaConfig{
Query: queryType,
Mutation: mutationType,
},
)
至此,我们的全部的object定义完成。
提供服务
graphql-go为我们提供了一个方便的接口,封装好的handler可以直接与go自带的http包绑定。
package api
import “github.com/graphql-go/handler”
func Register() *handler.Handler {
h := handler.New(&handler.Config{
Schema: &schema,
Pretty: true,
GraphiQL: true,
})
return h
}
func main() {
h := api.Register()
handler := cors.Default().Handler(h)
http.Handle(“/graphql”, handler)
fmt.Println(“The api server will run on port : “, apiPort)
http.ListenAndServe(apiPort, nil)
}
打开浏览器,访问http://localhost:apiPort/graphql, 查看你自己的GraphiQL界面吧!
结束语
如果你觉得这样的代码谈不上优雅,甚至非常丑陋,那就对了。因为我也这样觉得,看一看隔壁python的实现方式:
import graphene
class Query(graphene.ObjectType):
hello = graphene.String()
def resolve_hello(self, args, context, info):
return ‘Hello world!’
schema = graphene.Schema(query=Query)
有没有涌来一口老血。
可能是受限与golang本身反射系统并不够完善,没有python各种各样的魔术方法,没有泛型,或者说go本身不太适合编写框架类的代码。在编写的过程中,冗余非常多,当然也可能是框架本身的问题
不可否认的是,go确实是非常不错的一门语言,虽然开发效率无法与python媲美,但是在多并发环境下,go表现出非常出色,同时拥有与C级别的运行速度和丰富的生态。
https://studygolang.com/articles/12644
https://studygolang.com/articles/19225?fr=sidebar
https://graphql.cn/