GraphQL/Mongoose:如何防止每个请求字段调用一次数据库
Posted
技术标签:
【中文标题】GraphQL/Mongoose:如何防止每个请求字段调用一次数据库【英文标题】:GraphQL/Mongoose: How to prevent calling the database once per requested field 【发布时间】:2021-12-07 18:50:06 【问题描述】:我是 GraphQL 的新手,但我的理解是,如果我有一个 User
类型,例如:
type User
email: String
userId: String
firstName: String
lastName: String
还有这样的查询:
type Query
currentUser: User
像这样实现解析器:
Query:
currentUser:
email: async (_: any, __: any, ctx: any, ___: any) =>
const provider = getAuthenticationProvider()
const userId = await provider.getUserId(ctx.req.headers.authorization)
const email = await UserService.getUserByFirebaseId(userId)
return email;
,
firstName: async (_: any, __: any, ctx: any, ___: any) =>
const provider = getAuthenticationProvider()
const userId = await provider.getUserId(ctx.req.headers.authorization)
const firstName = await UserService.getUserByFirebaseId(userId)
return firstName;
// same for other fields
,
很明显出了点问题,因为我复制了代码,并且每个请求的字段都查询了一次数据库。有没有办法防止代码重复和/或缓存数据库调用?
我需要填充 MongoDB 字段的情况如何?谢谢!
【问题讨论】:
提供有关您拥有的代码的更多信息将帮助人们帮助您。至少提供代码部分,展示如何使用类型、查询和解析器的示例,包括使用过的导入。 【参考方案1】:有没有办法防止代码重复和/或缓存数据库调用?
首先,这个
const provider = getAuthenticationProvider()
实际上应该注入到graphql服务器请求的上下文中,这样你就可以在解析器中使用它,例如:
ctx.authProvider
其余部分遵循 Dan Crews 的回答。父解析器,最好使用数据加载器。在这种情况下,您实际上不需要 authProvider
并且将仅使用数据加载器,具体取决于实体类型并通过从上下文传递额外变量(如用户 ID)
【讨论】:
【参考方案2】:一些事情:
1a。父解析器
作为一般规则,任何给定的解析器都应该返回足够的信息来解析子节点的值,或者返回足够的信息让子节点自行解析。通过Ruslan Zhomir 回答。这会进行一次数据库查找并为子项返回这些值。好处是您不必复制任何代码。缺点是数据库必须获取所有字段并返回它们。那里有一个权衡取舍的平衡行为。大多数时候,您最好为每个对象使用一个解析器。如果您开始不得不从其他位置处理数据或提取字段,那么我通常会开始像您一样添加字段级解析器。
1b。字段级解析器
您仅显示字段级解析器(没有父对象解析器)的模式可能很尴尬。以你为例。如果用户未登录,“预期”会发生什么?
我希望得到以下结果:
currentUser: null
但是,如果您只构建字段级解析器(没有实际在数据库中查找的父解析器),您的响应将如下所示:
currentUser:
email: null,
userId: null,
firstName: null,
lastName: null
另一方面,如果您实际上在数据库中查看了足够长的时间来验证用户是否存在,那么为什么不返回该对象呢?这是我推荐单亲解析器的另一个原因。同样,一旦您开始处理其他数据源或其他属性的昂贵操作,这就是您要开始添加子解析器的地方:
const resolvers =
Query:
currentUser: async (parent, args, ctx, info)
const provider = getAuthenticationProvider()
const userId = await provider.getUserId(ctx.req.headers.authorization)
return UserService.getUserByFirebaseId(userId);
,
User:
avatarUrl(parent)
const hash = md5(parent.email)
return `https://www.gravatar.com/avatar/$hash`;
,
friends(parent, args, ctx)
return UsersService.findFriends(parent.id);
2a。数据加载器
如果你真的很喜欢子属性解析器模式(PayPal 有一位主管 EATS IT UP,DataLoader 模式 (and library) 使用带有缓存键的记忆来对数据库进行一次查找并缓存该结果。每个解析器要求服务获取用户(“这里是 firebaseId”),并且该服务缓存响应。您拥有的解析器代码将是相同的,但在后端执行数据库查找的功能只会发生一次,而其他人从缓存中返回。你在这里展示的模式是我见过人们做的模式,虽然它通常是过早的优化,但它可能是你想要的。如果是这样,DataLoaders 就是一个答案。如果你不这样做不想走重复代码或“神奇的解析器对象”的路线,你最好只使用一个解析器。
另外,请确保您没有成为上述“空对象”问题的受害者。如果父级不存在,则父级应该为空,而不仅仅是所有子级。
2b。数据加载器和上下文
小心使用 DataLoader。该缓存可能存在时间过长或为无权访问的人返回值。因此,通常建议为每个请求创建数据加载器。如果您查看 DataSources (Apollo),它遵循相同的模式。该类在每个请求上都被实例化,并且对象被添加到上下文中(在您的示例中为ctx
)。您可以在请求范围之外创建其他数据加载器,但是如果您走这条路,您必须解决 Least-Used 和 Expiration 以及所有这些问题。这也是您需要更进一步的优化。
【讨论】:
【参考方案3】:我会像这样重写你的解析器:
// import ...;
type User
email: String
userId: String
firstName: String
lastName: String
type Query
currentUser: User
const resolvers =
Query:
currentUser: async (parent, args, ctx, info)
const provider = getAuthenticationProvider()
const userId = await provider.getUserId(ctx.req.headers.authorization)
return UserService.getUserByFirebaseId(userId);
;
这应该可以工作,但是...如果有更多信息,代码也可能会更好(请参阅我的comment)。
您可以在此处阅读有关解析器的更多信息:https://www.apollographql.com/docs/apollo-server/data/resolvers/
【讨论】:
以上是关于GraphQL/Mongoose:如何防止每个请求字段调用一次数据库的主要内容,如果未能解决你的问题,请参考以下文章
javascript [basic graphql-yoga]连接到monoDB #graphql #mongoose #mongoDB