当前位置: 首页 > 后端技术 > Node.js

GraphQL界面设计

时间:2023-04-04 01:40:14 Node.js

文章原文请移至我的博客:GraphQL界面设计graphql是一种针对API的查询语言。提供完整易懂的API接口数据描述,让客户无需实现其他更多代码,即可准确查询所需数据,让API接口开发更简单、更高效。最近在用Gatsby开发一版静态博客,感觉没接触过这个框架。因为这个框架使用了graphql技术,所以花了一些时间学习,把学习和思考的过程记录下来。本文主要讲解如何理解graphql以及基于关系型数据库设计graphql的思路。如果需要学习graphql基础知识,请移步官方文档本文包含Nestjs+graphql的示例工程:https://github.com/YES-Lee/nestjs-graphql-starter了解graphqlgraphql是一个工具用于描述数据及其关系的查询语言,官方文档中描述了graphql的标准,具体实现依赖于第三方。目前主流方案是Apollo提供的方案。graphql并不关心我们是如何获取数据的,我们只需要提供获取数据的方法(resolver)以及如何组装数据(schema)即可。类似于Java开发过程中的接口设计模式,Graphql定义了一套标准,我们按照标准来实现接口。让我们以用户角色模型为例。下面的代码定义了两种数据结构,类似于JSON,应该很容易理解。它描述了每种类型的名称,以及它包含的属性。除了基本类型,属性还可以是数组等引用类型,从而建立所有的数据模型和相互关系图。typeRole{name:Stringnote:String}typeUser{id:Intname:Stringgender:Introle:Role}上面的代码是用来描述Role和User的数据结构的,那么我们具体怎么使用这个东西呢?站在前端的角度,可以从官方文档中了解前端的基本用法。请求体中的数据描述与上面定义的代码略有不同。比如我们要查询用户数据:queryuserInfo{user(id:Int){idnamegenderrole{namenote}}}从上面的代码我们可以大致猜到如果不需要查询角色数据,我们只需要从请求queryuserInfo{user(id:Int){idnamegender}}中去掉即可,当请求到达服务器后,graphql会解析请求体。当它解析到用户时,它会执行我们定义的逻辑来获取用户数据。解析到角色也是如此。那么我们就要在server端定义获取数据的逻辑,在graphql中称为resolver。前面的代码只是定义了数据结构,我们还需要为它创建一个解析器来获取相应的数据,类似下面的代码functionuserResolver(id){//...//returnUser}functionroleResolver(userId){//...//returnRole}我们可以在解析器中执行任何能够获取到所需数据的逻辑,比如sql、http请求、rpc通信等。最后只需要按照预定义的结构返回数据即可。Schema前面用来定义不同类型数据结构的代码称为Schema。它是graphql用来描述数据结构和关系的一种语言。在Schema的类型定义中,可以使用graphql定义的标量类型(ScalarTypes)如Int、Float、String、Boolean、ID等,也可以使用联合类型来引用其他A类型等,比如上面代码User中引用的Role。ResolverResolver是GraphQL中用来获取数据的方法。它不关心Resolver的具体实现,我们可以从数据库、HTTP接口、服务器资源等渠道获取数据。graphql会根据请求中声明的字段执行解析器,一定程度上可以减少查询次数。在下面的代码中,会执行UserResolver,然后继续执行RoleResolver{User{namerole}}如果我们去掉role字段,服务器就不会再执行RoleResolver{User{name}}UserResolver执行完后,会返回数据给前端。可见graphql可以动态执行数据查询,减少不必要的资源消耗。然而,任何事情都有两个方面。换个角度思考,我们如何控制Resolver的粒度呢?假设我们的Resolver在进行数据库查询,在restfulAPI中,通常我们会使用关联查询同时获取User和Role数据。但是在graphql中,我们为每个关联对象创建一个Resolver。当我们查询一个用户列表,需要包含用户相关的角色时,就会发现一个问题。一个请求需要执行N+1个SQL查询:1个UserResolver获取N个用户的列表,N个查询获取每个用户的角色。这就是后面要讲的N+1问题。graphqlN+1问题N+1问题不仅仅存在于graphql中,我们写sql的时候也会有类似的情况,但是我们会通过关联查询来避免这个问题。为了解决N+1问题,graphql官方提供了dataloader的解决方案。Dataloader使用了缓存技术,也将多个相同(相似)的请求合并,将上面提到的N个查询合并为一个。基本原理是先进行查询列表的操作,然后将每条记录的关联字段作为参数列表。通过一次查询得到所有的关联数据后,合并到上层数据中。Dataloader是目前解决N+1问题的有效方法,使用起来难度不大。其次,我们可以通过控制Resolver的粒度来减少查询次数。比如前面的例子,不用写roleResolver,直接通过一个query的关联查询就可以得到user和role。当然,如果这样做了,不管前端是否查询角色字段,服务都会进行关联查询。这里需要根据具体场景进行选择。HTTP缓存问题由于graphql接口请求只有一个统一的端点,我们无法使用HTTP缓存。目前的一些前端实现,比如Apollo,提供了inMemeryCache的方案,但是用户体验不是很友好。关系型数据库+graphql接口设计对graphql有了一个大概的了解之后,我们就开始关系型数据库(Mysql)+graphqlAPI的设计和schema的编写。对于schema设计,首先忽略表之间的关系,优先建立对应的数据表Model。比如User表和Role表的创建方式如下即敏感数据不能出现在敏感信息(用户密码等)中,即使Resolver结果包含此类敏感信息,只要schema中不包含这些字段,graphql就会自动过滤这些字段。建立好所有表对应的schema之后,再考虑表之间的关系。表关系的处理graphql的关系处理和RestfulAPI有一些区别。在RestfulAPI中,我们一般只在需要的接口中建立关系查询。找出您需要的所有信息。但是在graphql中,我们应该将所有表的关系描述为一个图结构,并保证所有有关系(一对多或多对多)的表对应的schema是连接在一起的,这样当我们请求,我们可以从一个节点到任何一个和它有关系的节点。这也是我认为graphql的一大魅力。当我们建立了完整的关系图后,前端就可以自由查询和组合数据了。从理论上讲,前端可以无限递归查询一组数据,比如:小明->小明的朋友->小明的朋友的朋友->...,我们只需要选择一个好的起点就可以到达任何地方。一对多关系一对多关系的建立非常简单。我们只需要编写相应的Resolver,然后在主表对应的schema中添加字段即可。typeRole{name:Stringnote:String}typeUser{name:Stringgender:Introle:Role}functionuserResolver(){//...//returnUser}functionroleResolver(userId){//...//returnRole}我们写完roleResolver之后,在User中加入了role字段,当请求这个字段的时候,graphql会执行roleResolver获取数据,我们来看多对多关系的处理。多对多关系通常,在RestfulAPI中,我们会通过一个SQL相关查询来获取多对多的相关数据,但是在graphql中,如果仅仅使用相关查询,显然没有充分发挥其特性。我们看下面的例子#schematypeUser{name:Stringgender:Introle:Rolegroups:[Group]#用户组userGroups:[UserGroups]#用户组关系表}typeGroup{id:Intname:Stringnote:Stringusers:[User]userGroups:[UserGroups]}typeUserGroup{id:IntuserId:IntgroupId:Intnote:String#在关系表中存储一些关联信息user:Usergroup:Group}对于上面的schema,可以看User中包含groups和userGroups,同理Group中也包含users和userGroups。User和Group都包含在UserGroup中,所以我们可以这样查询{user(id:1){namegroups{idnamenoteuserGroups{iduserIdgroupIdnoteuser{namegroups{#...}}}}}}有人可能会问,上面的操作是在死循环。没错,确实是死循环。这不是bug,而是我前面提到的建立联合关系。针对不同的场景,我们可以采用不同的方式进行查询。例如,当我需要搜索用户组时,我可以在组中添加一些参数{user(id:1){namegroups(name:"admin"){idnamenoteuserGroups(userId:1){idnote}对于上面的查询,如果我们只想在UserGroup关系表中查找额外的信息,上面的查询方式是行不通的。那么我们可以从另一个方向查询{user(id:1){nameuserGroups(note:"newuser"){iduserIdgroupIdnotegroup{idnamenote}}}}可以找到,通过建立对应关系连接图后,我们可以从一张表查询到与其相关的任意一张表,同时可以无限嵌套查询。不用担心死循环问题,因为我们需要指定关联的字段,graphql才会执行对应的Resolver。如果有死循环,除非我们的query也写成死循环,显然这是不可能的。这基本上就是关系管理的内容。如果大家有更好的想法,欢迎大家骚扰。结论这篇文章是我在学习和使用graphql过程中的实践和思考。如有错误或建议,请联系本人指正和讨论。另外,在实践之前,要重点关注是否需要使用graphql,因为restfulapi已经可以满足大部分场景的需求,盲目使用graphql可能会带来一些意想不到的问题。