百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Graphql中我们应该用什么姿势来实现Resolver?

ccwgpt 2025-05-07 23:25 17 浏览 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!
}

接下来我们就开始定义 QueryMutation 这种用来提供给前端使用的 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方法,第一次是 QueryallArticlesResolver,第二次 Article->CommentResolver

所以我们简单定义实现一下Article->CommentResolver:

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 引擎就自动的帮助我们组装好了结构化的数据了,是不是非常方便?

关于 GraphQLScalarTypeGraphQL 中内置了像 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,可是我们还是去 joinComment,这没有丝毫意义,反而加重了数据库请求的负担,这就是没有做到按需 join

同时我们默认 select * from table,而 query 中我们仅仅只需要那么 1-2 个字段,这意味着大量的字段的数据,从数据库中取出了之后,加载进内存里,然后交给 Graphql 引擎处理之后,便被无情的抛弃了,其实从一开始就没有从数据库里取出它们的必要。比如 Article content 每个都有 1MB 呢?那内存不是很容易溢出?所以我们同时也要实现按需 select

那么我们怎么实现呢?接下来我们聚焦在一个对象上: GraphQLResolveInfo

GraphQLResolveInfo

GraphQLResolveInfo 这个对象里,包含着我们这次 Query 请求所有的模式和操作信息还有AST节点等等对象。

我们可以通过解析它来获取一次 Query 究竟要获取多少个字段,多少个不同的 Type 以及对应的层级,深度等等。在我们这种场景,我们只需要把 QueryServer 端被还原成对象,来供给我们做相对于的操作就行。

于是我们就能够封装一个方法来获取这个对象:

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+ Appium:Android手机连接与操作详解(附源码)

在移动端自动化测试领域,Appium一直是最热门的开源工具之一。今天这篇文章,我们聚焦Android端自动化测试的完整流程,从环境配置到代码实战,一步一步带你掌握用Python控制Android...

全平台开源即时通讯IM框架MobileIMSDK开发指南,支持鸿蒙NEXT

写在前面在着手基于MobileIMSDK开发自已的即时通讯应用前,建议以Demo工程为脚手架,快速上手MobileIMSDK!Demo工程主要用于演示SDK的API调用等,它位于SDK完整下载包的如下...

移动开发(一):使用.NET MAUI开发第一个安卓APP

对于工作多年的C#程序员来说,近来想尝试开发一款安卓APP,考虑了很久最终选择使用.NETMAUI这个微软官方的框架来尝试体验开发安卓APP,毕竟是使用VisualStudio开发工具,使用起来也...

在安卓系统上开发一款软件详细的流程

安卓app软件开发流程是一个系统而复杂的过程,涉及多个阶段和环节。以下是一个典型的安卓软件开发流程概述:1.需求分析目的:了解用户需求,确定APP的目标、功能、特性和预期效果。活动:开发团队与客户进...

ArkUI-X在Android上使用Fragment开发指南

本文介绍将ArkUI框架的UIAbility跨平台部署至Android平台Fragment的使用说明,实现Android原生Fragment和ArkUI跨平台Fragment的混合开发,方便开发者灵活...

Web3开发者必须要知道的6个框架与开发工具

在Web3领域,随着去中心化应用和区块链的兴起,开发者们需要掌握适用于这一新兴技术的框架与开发工具。这些工具和框架能够提供简化开发流程、增强安全性以及提供更好的用户体验。1.Truffle:Truff...

Python开发web指南之创建你的RESTful APP

上回我们说到了:PythonFlask开发web指南:创建RESTAPI。我们知道了Flask是一个web轻量级框架,可以在上面做一些扩展,我们还用Flask创建了API,也说到了...

python的web开发框架有哪些(python主流web框架)

  python在web开发方面有着广泛的应用。鉴于各种各样的框架,对于开发者来说如何选择将成为一个问题。为此,我特此对比较常见的几种框架从性能、使用感受以及应用情况进行一个粗略的分析。  1Dja...

Qwik:革新Web开发的新框架(webview开源框架)

听说关注我的人,都实现了财富自由!你还在等什么?赶紧加入我们,一起走向人生巅峰!Qwik:革新Web开发的新框架Qwik橫空出世:一场颠覆前端格局的革命?是炒作还是未来?前端框架的更新迭代速度,如同...

Python中Web开发框架有哪些?(python主流web框架)

Python为Web开发提供了许多优秀的框架。以下是一些流行的PythonWeb框架:1.Django:一个高级的Web框架,旨在快速开发干净、实用的Web应用。Django遵...

WPF 工业自动化数据管控框架,支持热拔插 DLL与多语言实现

前言工业自动化开发中,设备数据的采集、处理与管理成为提升生产效率和实现智能制造的关键环节。为了简化开发流程、提高系统的灵活性与可维护性,StarRyEdgeFramework应运而生。该框架专注...

[汇川PLC] 汇川IFA程序框架06-建立气缸控制FB块

前言:汇川的iFA要跟西门子对标啦,这可是新的选择!就在2月14日,汇川刚发布的iFA平台,一眼就能看出来是对标西门子的全集成自动化平台博途(TIAPortal)。这个平台能在同一个...

微软发布.NET 10首个预览版:JIT编译器再进化、跨平台开发更流畅

IT之家2月26日消息,微软.NET团队昨日(2月25日)发布博文,宣布推出.NET10首个预览版更新,重点改进.NETRuntime、SDK、libraries、C#、AS...

大模型部署革命:GGUF量化+vLLM推理的极致性能调优方案

本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在官网-聚客AI学院大模型应用开发微调项目实践课程学习平台一、模型微调核心概念与技术演进1.1微调的本质与优势数学表达:1....

拓扑学到底在研究什么?(拓扑学到底在研究什么问题)

拓扑是“不量尺寸的几何学”,那么它的核心内容,主要方法是什么?如果你问罗巴切夫斯基,他会说“附贴性是物体的一个特殊的属性。如果我们把这个性质掌握,而把物体其他的一切属性,不问是本质的或偶然出现的,均不...

取消回复欢迎 发表评论: