如何解决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 举报,一经查实,本站将立刻删除。