Graphql中我们应该用什么姿势来实现Resolver?
ccwgpt 2025-05-07 23:25 3 浏览 0 评论
前言
我最近在用 Graphql 来弥补原先写的 RESTFUL 接口的一些短板。在实践过程中遇到了一些思考,借着文章抛砖引玉,分享给大家。
为了让大家更好的理解本文的思想,我搞了一个简单的案例,源码见附录。
设计数据库
先上一个关系型数据库 ER 图。既然用 Graphql 来做数据聚合和查询,那么我们先从数据库表的设计开始,毕竟这才是数据的源头。
从上图可知这些实体之间所有的关系信息,接着根据ER图我们来定义 Graphql Type
定义 Type
首先我们把 ER 图,变成 Graph TypeDefs 即为:
于是就能直接粗略的把 SDL 写好了:
type Article { comments: [Comment!]! content: String! id: Int! tags: [Tag!]! title: String! topic: Topic user: User! userId: Int! } type Comment { article: Article! articleId: Int! content: String! id: Int! user: User! userId: Int! } type Tag { articles: [Article!]! id: Int! name: String! } type Topic { article: Article! articleId: Int! id: Int! name: String! } type User { articles: [Article!]! comments: [Comment!]! id: Int! name: String! }
接下来我们就开始定义 Query 和 Mutation 这种用来提供给前端使用的 entry point 了。至此,新手部分结束,进入本篇的正文。
实现 Resolver
当我们定义好所有的 Type 之后,接下来就是去真正的实现后端的交互逻辑,也就是实现 Resolver。
那么什么是 Resolver 呢?Resolver实际上就是一个函数,它负责为我们定义的 schema 中每个字段来填充相应的数据。
那么我们需要实现哪些呢?显然每一个 GraphQL Type 都必须定义一个 Resolver 用来去获取对应格式的数据. 比如 Article 从数据表中获取就可以这么写:
添加一个 Query:
type Query { allArticles: [Article] }
实现对应的 Resolver
为了易于展示和理解,接下来的数据库交互部分代码都使用 prisma orm 框架来表示
Query: { // 入参依次为 parent 节点,这里为 undefined // 参数args // 上下文ctx // GraphQLResolveInfo info allArticles(_, args, ctx, info) { return prisma.article.findMany() }}
对于 Graphql 引擎来说,它会把 prisma.article.findMany() 的结果,转化成定义的 [Article]。针对2者同名字段的处理,当发现是一个 Scalar 的时候,就会去使用内部的序列化方法来处理数据。当请求query里包含另一个子 Type 时,它就会执行当前Type下对应向量的 Resolver函数。
我们来看一个例子:
query { allArticles{ id # 当前 type 是 Article comments{ # 当前 type 是 Comment id } } }
这个请求,就以此调用了2个Resolver方法,第一次是 Query 的 allArticles 的 Resolver,第二次 Article->Comment 的 Resolver。
所以我们简单定义实现一下Article->Comment 的 Resolver:
Article: { comments(parent: Article, args, ctx, info) { return prisma.comment.findMany( where: { articleId: parent.id } ) }},
和之前定义的Query allArticles Resolver 不同,这时候的 Resolver函数,它是有 parent 节点的,这个 parent 就是上一个 allArticles Resolver 的数组返回结果中单个的 Article。我们传入一个 where 筛选条件,来在数据库中筛选指定条件的数据,返回出来,再经过 Comment 中,每一个 Scalar 的序列化,最终组装到父节点的数据对象上。
最终 Graphql 引擎就自动的帮助我们组装好了结构化的数据了,是不是非常方便?
关于 GraphQLScalarType,GraphQL 中内置了像 Int,Float,String,Boolean,ID 这类的 GraphQLScalarType 用于基础的声明与数据的处理。我们也可以自定义 GraphQLScalarType,当然 graphql-scalars 内已经实现了许多开箱即用的 Scalar,推荐使用。
按需组装查询语句请求数据库
上面那个Demo明眼人一眼就能看出非常不好。比如上面那个查询语句,它的语义化结果,就是找到所有文章的id,以及每篇文章中所有评论的id。然而它的调用数据库查询次数太多了!比如你有 100 篇文章,每篇文章都要插一次评论,那就是 1+100=101 次查询,我的天啊,这对于列表查询是无法忍受的!
一般我们实现 RESTFUL 列表分页查询接口,我们会使用数据库的 join 或者 lookup 操作来处理这种情况。
那么好办了,我们可以在第一次调用 allArticles Resolver 的时候就去 join Comment表把数据统统取出来不就可以了吗?我们改一下实现:
Query: { allArticles(_, args, contextValue, info) { return prisma.article.findMany({ include: { comments: true } }) }}Article: { comments(parent, args, contextValue, info) { // 这里只是一个简略的判断,具体按类型进行判断 if (parent.comments) { return parent.comments } return prisma.comment.findMany({ where: { articleId: parent.id } }) }}
这一下子就把数据库的查询次数,降低到一次了,因为即使没有找到 Article 对应的 Comment,也会返回一个空数组,从而走了直接 return 的逻辑,这似乎就完成了我们的目标?
不,路漫漫其修远兮。假设又有 query 进来是这种呢?
query { allArticles{ id content } }
这次的请求可不需要 comments,可是我们还是去 join 了 Comment,这没有丝毫意义,反而加重了数据库请求的负担,这就是没有做到按需 join。
同时我们默认 select * from table,而 query 中我们仅仅只需要那么 1-2 个字段,这意味着大量的字段的数据,从数据库中取出了之后,加载进内存里,然后交给 Graphql 引擎处理之后,便被无情的抛弃了,其实从一开始就没有从数据库里取出它们的必要。比如 Article content 每个都有 1MB 呢?那内存不是很容易溢出?所以我们同时也要实现按需 select。
那么我们怎么实现呢?接下来我们聚焦在一个对象上: GraphQLResolveInfo
GraphQLResolveInfo
GraphQLResolveInfo 这个对象里,包含着我们这次 Query 请求所有的模式和操作信息还有AST节点等等对象。
我们可以通过解析它来获取一次 Query 究竟要获取多少个字段,多少个不同的 Type 以及对应的层级,深度等等。在我们这种场景,我们只需要把 Query 在 Server 端被还原成对象,来供给我们做相对于的操作就行。
于是我们就能够封装一个方法来获取这个对象:
import { parse, simplify, type ResolveTree } from 'graphql-parse-resolve-info'import type { GraphQLResolveInfo } from 'graphql'export type FieldInfo<T> = Partial<Record<keyof T, ResolveTree>>export function getFields<T = any>(info: GraphQLResolveInfo): FieldInfo<T> { const parsedResolveInfoFragment = parse(info) as ResolveTree const { fields } = simplify(parsedResolveInfoFragment, info.returnType) return fields}
获取这个层级对象之后,就能够对 Resolver 进行进一步的改进:
Query: { allArticles(_, args, contextValue, info) { const fields = getFields<Prisma.ArticleSelect>(info) const commentFields = fields.comments?.fieldsByTypeName.Comment const select: Prisma.ArticleSelect = { id: true, content: Boolean(fields.content), title: Boolean(fields.title), userId: Boolean(fields.userId || fields.user) } if (commentFields) { select.comments = { select: { id: true, content: Boolean(commentFields.content) } } } return prisma.article.findMany({ select }) },},Article: { comments( parent: Prisma.ArticleGetPayload<{ include: { comments: true } }>, args, contextValue, info ) { if (parent.comments) { return parent.comments } const fields = getFields<Prisma.CommentSelect>(info) return prisma.comment.findMany({ select: { id: true, articleId: Boolean(fields.articleId || fields.article), content: Boolean(fields.content), userId: Boolean(fields.userId || fields.user) }, where: { articleId: parent.id } }) }},
当然,这种解法虽然带来了按需的方式,但是同时要比之前那种写法要复杂的多。同时随着 Query 请求层级加深,代码会变得难以维护。
另外目前这种方式,在遇到复杂请求的时候也会出现问题,比如这种 Query:
query { allArticles { ...queryFragment } } fragment queryFragment on Article { comments { article { comments { article { comments { articleId } } } } } }
这种循环嵌套的请求在业务上没有啥意义,但却是合法可解析可运行的。
又比如遇到这种 Query 我们又要在代码里实现join User 表来取数据的逻辑:
query { allArticles{ id comments { id content user { id name } } } }
显然,这种按需方式大大的增加了实现的复杂度,而且也容易出错,那么究竟怎么做到性能和简单的平衡呢?
这是一个值得思考的问题,我有一个想法,既然要简单,那肯定是最初的那种101次调用数据库的方式最简单,假设这 101 次的数据都在缓存里呢?调用足够快,一次 redis pipe 调用就能把所有数据取出来,同时假如没有的数据,会自己去从数据库里,进行同步,置缓存之后再返回。
同时也有设计合理的数据预热机制与缓存刷新机制,那架构应该是怎么样的呢?如图所示:
同时在预热和数据库请求方面,也要尽量的合并请求来减小调用次数。这个问题就留到我们以后去探讨了。也欢迎大家分享各自的思考和想法。
附录
apollo-graphql-prisma-template
相关推荐
- Python Scrapy 项目实战(python scripy)
-
爬虫编写流程首先明确Python爬虫代码编写的流程:先直接打开网页,找到你想要的数据,就是走一遍流程。比如这个项目我要爬取历史某一天所有比赛的赔率数据、每场比赛的比赛结果等。那么我就先打开这个网址...
- 为何大厂后端开发更青睐 Python 而非 Java 进行爬虫开发?
-
在互联网大厂的后端开发领域,爬虫技术广泛应用于数据收集、竞品分析、内容监测等诸多场景。然而,一个有趣的现象是,相较于Java,Python成为了爬虫开发的首选语言。这背后究竟隐藏着怎样的原因呢?让...
- 爬虫小知识,scrapy爬虫框架中爬虫名词的含义
-
在上一篇文章当中学记给大家展示了Scrapy爬虫框架在爬取之前的框架文件该如何设置。在上一篇文章当中,是直接以代码的形式进行描述的,在这篇文章当中学记会解释一下上一篇文章当中爬虫代码当中的一些名词...
- python爬虫神器--Scrapy(python爬虫详细教程)
-
什么是爬虫,爬虫能用来做什么?文章中给你答案。*_*今天我们就开发一个简单的项目,来爬取一下itcast.cn中c/c++教师的职位以及名称等信息。网站链接:http://www.itcast.cn...
- Gradio:从UI库到强大AI框架的蜕变
-
Gradio,这个曾经被简单视为PythonUI库的工具,如今已华丽转身,成为AI应用开发的强大框架。它不仅能让开发者用极少的代码构建交互式界面,更通过一系列独特功能,彻底改变了机器学习应用的开发和...
- 研究人员提出AI模型无损压缩框架,压缩率达70%
-
大模型被压缩30%性能仍与原模型一致,既能兼容GPU推理、又能减少内存和GPU开销、并且比英伟达nvCOMP解压缩快15倍。这便是美国莱斯大学博士生张天一和合作者打造的无损压缩框架...
- 阿里发布Qwen-Agent框架,赋能开发者构建复杂AI智能体
-
IT之家1月4日消息,阿里通义千问Qwen推出全新AI框架Qwen-Agent,基于现有Qwen语言模型,支持智能体执行复杂任务,并提供多种高级功能,赋能开发者构建更强大的AI...
- 向量数仓与大数据平台:企业数据架构的新范式
-
在当前的大模型时代,企业数据架构正面临着前所未有的挑战和机遇。随着大模型的不断发布和多模态模型的发展,AIGC应用的繁荣和生态配套的逐渐完备,企业需要适应这种新的数据环境,以应对行业变革。一、大模型时...
- 干货!大数据管理平台规划设计方案PPT
-
近年来,随着IT技术与大数据、机器学习、算法方向的不断发展,越来越多的企业都意识到了数据存在的价值,将数据作为自身宝贵的资产进行管理,利用大数据和机器学习能力去挖掘、识别、利用数据资产。如果缺乏有效的...
- 阿里巴巴十亿级并发系统设计:实现高并发场景下的稳定性和高性能
-
阿里巴巴的十亿级并发系统设计是其在大规模高并发场景下(如双11、双12等)保持稳定运行的核心技术框架。以下是其关键设计要点及技术实现方案:一、高可用性设计多数据中心与容灾采用多数据中心部署,通过异地容...
- 阿里云云原生一体化数仓—数据治理新能力解读
-
一、数据治理中心产品简介阿里云DataWorks:一站式大数据开发与治理平台架构大图阿里云DataWorks定位于一站式的大数据开发和治理平台,从下图可以看出,DataWorks与MaxCom...
- DeepSeek R1:理解 GRPO 和多阶段训练
-
人工智能在DeepSeekR1的发布后取得了显著进步,这是一个挑战OpenAI的o1的开源模型,在高级推理任务中表现出色。DeepSeekR1采用了创新的组相对策略优化(GroupR...
- 揭秘永久免费视频会议软件平台架构
-
如今视频会议已经成为各个团队线上协同的必备方式之一,视频会议软件的选择直接影响团队效率与成本,觅讯会议凭借永久免费迅速出圈,本文将从技术架构、核心功能和安全体系等维度,深度解析其技术实现与应用价值,为...
- DeepSeek + Kimi = 五分钟打造优质 PPT
-
首先,在DeepSeek中输出提示词,示例如下:为课程《提示词基础-解锁AI沟通的秘密》设计一个PPT大纲,目的是让学生:1.理解提示词的概念、作用和重要性2.掌握构建有效提示词的基本原则和技巧...
- 软件系统如何设计可扩展架构?方法论,Java实战代码
-
软件系统如何设计可扩展架构?方法论,Java实战代码,请关注,点赞,收藏。方法论那先想想方法论部分。扩展性架构的关键点通常包括分层、模块化、微服务、水平扩展、异步处理、缓存、负载均衡、分布式架构等等...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- MVC框架 (46)
- spring框架 (46)
- 框架图 (58)
- bootstrap框架 (43)
- flask框架 (53)
- quartz框架 (51)
- abp框架 (47)
- jpa框架 (47)
- laravel框架 (46)
- express框架 (43)
- springmvc框架 (49)
- 分布式事务框架 (65)
- scrapy框架 (56)
- java框架spring (43)
- grpc框架 (55)
- orm框架有哪些 (43)
- ppt框架 (48)
- 内联框架 (52)
- winform框架 (46)
- gui框架 (44)
- cad怎么画框架 (58)
- ps怎么画框架 (47)
- ssm框架实现登录注册 (49)
- oracle字符串长度 (48)
- oracle提交事务 (47)