GraphQL解析器应该有多懒?

如何解决GraphQL解析器应该有多懒?

GraphQL解析器应该有多懒?

对于某些情况,这是我的体系结构的鸟瞰图:GraphQL->解析器-> |域边界| ->服务->加载程序->数据源(Postgres / Redis / Elasticsearch)

通过域边界,没有GraphQL特定的构造。 服务代表域的各个方面,解析器仅处理SomeQueryInput,委托给适当的服务,然后使用操作结果构造适当的SomeQueryResult。所有业务规则(包括授权)都存在于域中。 加载程序提供对域对象的访问,并具有对数据源的抽象访问,有时使用DataLoader模式,有时不使用。

让我用一个场景来说明我的问题:假设有一个用户有一个项目,而一个项目有许多文档。一个项目也有许多用户,某些用户可能不被允许查看所有文档。

让我们构造一个模式,并执行一个查询以检索当前用户可以看到的所有文档。

type Query {
  project(id:ID!): Project
}

type Project {
  id: ID!
  documents: [Document!]! 
}

type Document {
  id: ID!
  content: String!
}
{
  project(id: "cool-beans") {
    documents {
      id
      content
    }   
  }
}
Assume the user state is processed outside of the GraphQL context and injected into the context.

以及一些相应的基础结构代码:

const QueryResolver = {
  project: (parent,args,ctx) => {
    return projectService.findById({ id: args.id,viewer: ctx.user });
  },}

const ProjectResolver = {
  documents: (project,ctx) => {
    return documentService.findDocumentsByProjectId({ projectId: project.id,viewer: ctx.user })
  }
}

const DocumentResolver = {
  content: (parent,ctx) => {
    let document = await documentLoader.load(parent.id);
    return document.content;
  }
}


const documentService => {
  findDocumentsByProjectId: async ({ projectId,viewer }) {
    /* return a list of document ids that the viewer is eligible to view */
    return getThatData(`SELECT id FROM Documents where projectId = $1 AND userCanViewEtc()`)
  }
}

因此查询执行将执行:解析项目,获取查看者有资格查看的文档列表,解析文档,并解析其内容。您可以想象DocumentLoader是超通用的,并且与业务规则无关:它的唯一工作就是尽可能快地获取ID对象。

select * from Documents where id in $1

我的问题围绕documentService.findDocumentsByProjectId。这里似乎有多种方法:现在,该服务已经包含了一些GraphQL知识:它返回所需对象的“存根”,知道它们将被解析为正确的对象。这会增强GraphQL域,但会削弱服务域。如果另一个服务调用了该服务,他们将获得一个无用的存根。

为什么不只有findDocumentsByProjectId可以执行以下操作:

SELECT id,name,content FROM "Documents" JOIN permisssions,etc etc

现在,该服务功能更强大,并且可以返回整个业务对象,但是GraphQL域变得更加脆弱:您可以想象更复杂的场景,其中以一种服务所不希望的方式查询GraphQL模式,结果查询损坏和数据丢失。现在,您也可以...删除您编写的解析器,因为大多数服务器将琐碎地解析这些已经水化的对象。您已向REST端点方法退了一步。

另外,第二种方法可以利用用于特定目的的数据源索引,而DataLoader使用更强力的WHERE IN方法。

您如何平衡这些担忧?我知道这可能是个大问题,但这是我一直在思考的很多事情。域模型是否缺少在这里可能有用的概念? DataLoader查询是否应该比仅使用通用ID更具体?我很难找到一个优雅的平衡点。

现在,我的服务同时具有:findDocumentStubs和findDocuments。第一个由解析器使用,第二个由其他内部服务使用,因为它们不能依赖GraphQL分辨率,但这也不是很正确。即使使用DataLoader进行批处理和缓存,也仍然感觉像有人在做不必要的工作。

解决方法

如果您正在编写这样的解析器

function resolveFullName ({ first_name,last_name }) => {
  return `${first_name} ${last_name}`;
}

那么你可能做错了事。

在这种情况下,您实际上要做的是将域逻辑从域层中拉出来,并将其注入到API层中。如果您遵循设计数据库的良好实践,那么数据层将运行成为无法直接使用的规范混乱。应用业务规则并将数据转换为可被应用程序其他部分使用的形状是您域层的工作。

您写道:

您现在还可以...删除您编写的解析器,因为大多数服务器将琐碎地解析这些已经水化的对象。您已向REST端点方法退了一步。

我认为这不是一个公平的评估。您仍在利用GraphQL将服务返回的各种域对象连接到单个图中。客户端应用程序仍然可以向您的API发出单个请求,并获取其所需的所有数据-您所做的工作与REST类似。

如果您关注的是优化数据库查询,那么您当然可以利用更复杂的DataLoader模式来实现该目标。服务公开的方法也可以接受字段数组作为参数,这将使您在选择“水合”域对象时更加选择要选择的列和进行的联接。 GraphQL解析器可以轻松地从GraphQLResolveInfo对象派生此字段数组,该对象作为第四个参数传递。

,

(经过一些研究并综合了@Daniel的一些建议,回答了我自己的问题)

让我尝试解决您的核心问题,该问题的重点是获取符合某些条件的馆藏。您感觉到的摩擦来自于获取文档ID的集合,然后转身进行类似的查询来解析这些文档中的其余字段。我认为感觉一开始是重复的工作是很合理的,尤其是对于GraphQL来说是新手:为什么不急于在第一个查询中从数据库中获取所有需要的字段?有充分的理由:

比方说,我们急切地获取了我们“知道”的文档数据:与其在ProjectResolver中获取ID列表,然后在DocumentResolver中再次获取以解析文档,我们还是在ProjectResolver中获取所有内容,然后让我们的GraphQL服务器轻松解析文档字段。这似乎工作正常,但是我们已经将文档解析的负担移交给了项目解析器。让我们添加一个类型为User的字段createdDocuments:[Document!]!..

type User {
  id: ID!
  name: String!
  createdDocuments: [Document!]!
}

在用户上查询创建的文档时会发生什么?没什么用,除非我们也有UserResolver来获取文档数据... 通过允许父母成为其子女的唯一数据源,我们迫使所有未来的父母也这样做。我们的GraphQL API脆弱且难以维护和扩展。如果我们只是让ProjectResolver变得懒惰而只返回最小的最小值,然后强制DocumentResolver进行与Documents有关的所有工作,那么我们就不会遇到这个问题。

从这两次往返数据库的旅程中仍然会有发痒的感觉。您可以通过更多地使用DataLoader并使用缓存启动来采用中间路径。 Facebook JS DataLoader实现有一个称为prime()的方法,该方法可让您将数据播种到加载程序的缓存中。如果您使用一堆DataLoader,则可能会有多个加载器在不同上下文中引用相同的对象。 (如果您使用Apollo Client进行前端工作,应该会感到熟悉)。当您在一个上下文中获取某个对象时,只需将其填充到其他上下文中即可作为后处理步骤。

在获取该项目的文档列表时,请继续并急切地获取内容,但要使用该结果来填充DocumentLoader。现在,当您的DocumentResolver启动时,它将准备好所有这些数据,但是如果没有预取的结果,它将仍然是自给自足的。您必须根据应用程序的需要在何时执行此操作时做出最佳判断。您还可以使用Daniel Rearden的建议并使用GraphQLResolveInfo来有条件地决定像这样进行预取,但请确保不要因进行微优化而陷入杂草。

想象一下,您有两个DataLoader:ProjectDocumentsLoader和DocumentLoader。 ProjectDocumentsLoader可以将DocumentLoader的结果预填充为后期处理步骤。我喜欢将DataLoaders封装在轻巧的抽象中,以进行预处理和后期处理。


class Loader {
  load(id) {
    let results = await this.loader.load(id)
    return this.postProcess(results);
  }
  
  postProcess(data) {
    return data;
  }

  prime(key,value) {
    this.dataLoader.prime(key,value);
  }
}

class ProjectDocumentsLoader extends Loader {
  constructor(context) {
    this.context = context;
    this.loader = new DataLoader(/* function to get collection of documents by project */);
  }
  
  postProcess(documents) {
    documents.forEach(doc => this.context.documentLoader.prime(doc.id,doc));
    return documents;
  }
}

class DocumentLoader extends Loader {
  constructor(context) {
    this.context = context;
    this.loader = new DataLoader(/* function to get documents by id */);
  }
}

最后的答案:您的GraphQL解析器应该超级懒惰,可以选择预取,只要它是一种优化,而不是事实的来源。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 <select id="xxx"> SELECT di.id, di.name, di.work_type, di.updated... <where> <if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 <property name="dynamic.classpath" value="tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams['font.sans-serif'] = ['SimHei'] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -> systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping("/hires") public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate<String
使用vite构建项目报错 C:\Users\ychen\work>npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-