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:如何防止每个请求字段调用一次数据库的主要内容,如果未能解决你的问题,请参考以下文章

嵌套对象的模式/解析 graphql/mongoose

graphql mongoose 返回 null

javascript [basic graphql-yoga]连接到monoDB #graphql #mongoose #mongoDB

GraphQL Mongoose:转换为 ObjectId 的值失败

基本的 graphQL Mongoose 设置

如何在 PHP 中有效地防止跨站请求伪造 (CSRF)