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

news2024/12/28 4:24:54

Image

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

  • Graphql中我们应该用什么姿势来实现Resolver?
    • 前言
    • 设计数据库
    • 定义 Type
    • 实现 Resolver
    • 按需组装查询语句请求数据库
    • GraphQLResolveInfo
    • 附录

前言

我最近在用 Graphql弥补原先写的 RESTFUL 接口的一些短板。在实践过程中遇到了一些思考,借着文章抛砖引玉,分享给大家。

为了让大家更好的理解本文的思想,我搞了一个简单的案例,源码见附录。

设计数据库

先上一个关系型数据库 ER 图。既然用 Graphql 来做数据聚合和查询,那么我们先从数据库表的设计开始,毕竟这才是数据的源头。

Image

从上图可知这些实体之间所有的关系信息,接着根据ER图我们来定义 Graphql Type

定义 Type

首先我们把 ER 图,变成 Graph TypeDefs 即为:

Image

于是就能直接粗略的把 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,推荐使用。

那些引用其他 TypeField 就组成了关系,如图所示:

Image

图中,每一个箭头都是关系,这个关系是有向的,A->BB->A,它们 虽然都在做 resolve,但是它们调用的是不同的函数方法。

这个关系可以指向其他的 Type 也可以指向自身。比如有可能会出现 A->A->A->A->... 这样无限递归的 query,也会出现那种 A->B->A->B->... 这样的请求,这种请求复杂度的计算和限制,留到以后Graphql AST章节再说。

按需组装查询语句请求数据库

上面那个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 调用就能把所有数据取出来,同时假如没有的数据,会自己去从数据库里,进行同步,置缓存之后再返回。

同时也有设计合理的数据预热机制与缓存刷新机制,那架构应该是怎么样的呢?如图所示:

Image

同时在预热和数据库请求方面,也要尽量的合并请求来减小调用次数。这个问题就留到我们以后去探讨了。也欢迎大家分享各自的思考和想法。

附录

apollo-graphql-prisma-template

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/458646.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

实战Websocket

实战Websocket&#xff1a;从入门到自闭 作为前端开发人员&#xff0c;我们经常需要使用 Websocket 实现实时通信功能&#xff0c;如聊天室、实时数据展示、游戏等。近期我在一家公司实习工作中&#xff0c;也遇到了使用 Websocket 的场景&#xff0c;所以开始了解 Websocket …

第四章 使用Maven:IDEA环境

1、创建 Project2、开启自动导入 TIP 各个 IDEA 不同版本在具体操作方面存在一定差异&#xff0c;这里我们以 2019.3.3 版本为例进行演示。其它版本大家灵活变通即可。 第一节 创建父工程 创建 Project 开启自动导入 创建 Project 后&#xff0c;IDEA 会自动弹出下面提示…

有史以来第一次利用 Kubernetes RBAC 攻击后门集群

我们最近发现了有史以来第一个证据&#xff0c;表明攻击者正在野外利用 Kubernetes (K8s) 基于角色的访问控制 (RBAC) 创建后门。 攻击者还部署了 DaemonSets 来接管和劫持他们攻击的 K8s 集群的资源。我们的研究表明&#xff0c;该活动正在积极针对至少 60 个野外集群。 这…

026:Mapbox GL加载矢量切片数据源

第026个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+mapbox中加载矢量切片数据源。将矢量源添加到地图。使用其 tileset URL(mapbox:// + tileset ID)添加任何 Mapbox 托管的 tileset。 直接复制下面的 vue+mapbox源代码,操作2分钟即可运行实现效果 文章目录 示例…

python@可变对象和不可变对象@按值传递和引用传递@python运行可视化工具

文章目录 可变对象和不可变对象&#x1f388;可视化工具&#x1f388;可变对象和idegeg变量名和内存地址&#x1f388;函数调用对参数的修改&#x1f602;Note 按值传递vs引用传递note&#x1f388;如何借助函数修改外部变量的值?Note 可变对象和不可变对象&#x1f388; 在Py…

这些不可不知的JVM知识

JVM是面试中必问的部分&#xff0c;本文通过思维导图以面向面试的角度整理JVM中不可不知的知识。 先上图&#xff1a; JVM必备知识 1、JVM基本概念 1.1、JVM是什么 JVM 的全称是 「Java Virtual Machine」&#xff0c;也就是我们耳熟能详的 Java 虚拟机。 JVM具备着计算机的…

vue3 Pinia快速入门

为什么是Pinia 怎么说呢&#xff0c;其实在过往的大部分项目里面&#xff0c;我并没有引入过状态管理相关的库来维护状态。因为大部分的业务项目相对来说比较独立&#xff0c;哪怕自身功能复杂的时候&#xff0c;可能也仅仅是通过技术栈自身的提供的状态管理能力来处理业务场景…

huggingface下载的.arrow数据集读取与使用说明

1.数据下载方式&#xff1a;load_dataset 将数据集下载到本地&#xff1a;&#xff08;此处下载的是一个物体目标检测的数据集&#xff09; from datasets import load_dataset # 下载的数据集名称, model_name keremberke/plane-detection # 数据集保存的路径 save_path da…

苹果Mac电脑清理垃圾软件卸载工具CleanMyMac X

最近刚刚入手了一台 M1 Macbook&#xff0c;因为不是很懂下载了很多软件&#xff0c;然后又卸载了一些&#xff0c;导致系统内存在很多垃圾文件&#xff0c;我也不知道怎么清理&#xff0c;后来查询了一些资料&#xff0c;大家都普遍推荐 CleanMyMac X&#xff0c;于是经过我一…

触摸屏是如何诞生的,它又是如何影响和改变着我们的生活?

芊芊玉指在小小的屏幕上滑动&#xff0c;天下事便了然于胸。这就是手机触摸屏给我们的生活带来的改变。 曾几何时&#xff0c;我们是生活在九宫格或者全键盘上的“拇指族”。一股浪潮席卷而来&#xff0c;手机上的实体按键都消失了&#xff0c;虚拟按键仅在需要时出现。触摸屏是…

论文实验1、安装tensorflow运行节点嵌入相关方法

还是官方的教程好使 使用 pip 安装 TensorFlow 只有三步 1.安装python&#xff0c;版本太高不行&#xff0c;在推荐版本里选最高的。 2.安装python虚拟环境venv python -m venv --system-site-packages .\venv .\venv\Scripts\activate 3.在虚拟环境里装tensorflow pip…

vue的watch侦听器、watch的属性 immediate(侦听属性)、deep(侦听一个对象)

1.什么是watch侦听器 watch侦听器允许开发者监视数据的变化&#xff0c;从而针对数据的变化做特定的操作。 语法格式如下: const vm new Vue({el: #app,data: { username: },watch: {//监听username值的变化// newVal 是"变化后的新值”&#xff0c;oldVal 是"变…

Golang每日一练(leetDay0046)

目录 136. 只出现一次的数字 Single Number &#x1f31f; 137. 只出现一次的数字 II Single Number II &#x1f31f;&#x1f31f; 260. 只出现一次的数字 III Single Number III &#x1f31f;&#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f3…

linux docker搭建Zfile

1.下载镜像 docker pull stilleshan/zfile2.创建挂载目录 mkdir -p /opt/docker/zfile #自定义路径3.运行 docker run -d --namezfile --restartalways -p 1111:8080 \-v /opt/docker/zfile/conf:/root/.zfile-v4 \-v /opt/docker/zfile/data:/root/zfile/data \stillesha…

C/C++每日一练(20230425)

目录 1. 成绩分布 ※ 2. 汇总区间 &#x1f31f; 3. 矩阵置零 &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 1. 成绩分布 原标题&#xff1a;统计某一单…

基础数据结构-顺序表

顺序表 顺序表定义结构体定义初始化扩容函数打印函数尾插和尾删头插和头删查找函数指定位置插入和删除顺序表销毁 顺序表定义 顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构&#xff0c;一般情况下采用数组存储。在数组上完成数据的增删查改。 顺序表又分为…

图像预处理方法

图像预处理 膨胀腐蚀概述 两个基本的形态学操作是腐 和膨胀。他们 的变体构成了开运算 &#xff0c;闭运算&#xff0c; 梯度等。 根据卷积核的大小前景的所有像素会腐 掉 变为 0 &#xff0c;所以前景物体会变小整幅图像的白色区域会减少。 对于去除白噪声很有用 也可以用来…

推荐系统搭建全程图文攻略

推荐系统搭建全程图文攻略 推荐系统架构简介 整体推荐架构图&#xff1a; 推荐整体从数据处理开始&#xff0c;默认数据从关系型数据到每天增量导入到hive&#xff0c;在hive中通过中间表和调用python文件等一系列操作&#xff0c;将数据处理为算法数学建模的入口数据&#x…

【SVN】在Windows系统上进行SVN的基本操作(检出,更新,提交,分支合并分支,还原,制造冲突以及解决冲突,忽略)

介绍 这里是小编成长之路的历程&#xff0c;也是小编的学习之路。希望和各位大佬们一起成长&#xff01; 以下为小编最喜欢的两句话&#xff1a; 要有最朴素的生活和最遥远的梦想&#xff0c;即使明天天寒地冻&#xff0c;山高水远&#xff0c;路远马亡。 一个人为什么要努力&a…

其实苹果知道自己离不开中国制造,因此悄悄给自己留了后路

苹果在加速离开中国&#xff0c;不过从苹果的做法却又可以看到它其实很清醒地认识到无法离开中国制造&#xff0c;因此它在力推印度制造的时候&#xff0c;其实并没拼尽全力&#xff0c;深刻认识到印度制造和印度市场与中国的差距。 一、印度制造和印度市场与中国的差距 2022年…