如何将 Mongoose 与 GraphQL 和 DataLoader 一起使用?

Posted

技术标签:

【中文标题】如何将 Mongoose 与 GraphQL 和 DataLoader 一起使用?【英文标题】:How to use Mongoose with GraphQL and DataLoader? 【发布时间】:2019-03-17 21:22:39 【问题描述】:

我使用 MongoDB 作为我的数据库和 GraphQL。我正在为我的模型使用 Mongoose。我意识到我的 GraphQL 查询很慢,因为一遍又一遍地加载相同的文档。我想使用DataLoader 来解决我的问题,但我不知道如何。

示例

假设我有以下模式,描述有朋友的用户:

// mongoose schema
const userSchema = new Schema(
  name: String,
  friendIds: [String],
)

userSchema.methods.friends = function() 
  return User.where("_id").in(this.friendIds)


const User = mongoose.model("User", userSchema)

// GraphQL schema
const graphqlSchema = `
  type User 
    id: ID!
    name: String
    friends: [User]
  

  type Query 
    users: [User]
  
`

// GraphQL resolver
const resolver = 
  Query: 
    users: () => User.find()
  

这是我数据库中的一些示例数据:

[
   id: 1, name: "Alice", friendIds: [2, 3] ,
   id: 2, name: "Bob", friendIds: [1, 3] ,
   id: 3, name: "Charlie", friendIds: [2, 4, 5] ,
   id: 4, name: "David", friendIds: [1, 5] ,
   id: 5, name: "Ethan", friendIds: [1, 4, 2] ,
]

当我执行以下 GraphQL 查询时:


  users 
    name
    friends 
      name
    
  

每个用户都加载了很多次。我希望每个用户 Mongoose 文档只加载一次。

什么不起作用

为获取朋友定义一个“全局”数据加载器

如果我将friends 方法更改为:

// mongoose schema
const userSchema = new Schema(
  name: String,
  friendIds: [String]
)

userSchema.methods.friends = function() 
  return userLoader.load(this.friendIds)


const User = mongoose.model("User", userSchema)

const userLoader = new Dataloader(userIds => 
  const users = await User.where("_id").in(userIds)
  const usersMap = new Map(users.map(user => [user.id, user]))
  return userIds.map(userId => usersMap.get(userId))
)

然后我的用户将被永久缓存,而不是基于每个请求。

在解析器中定义数据加载器

This seems more reasonable : one caching mechanism per request.

// GraphQL resolver
const resolver = 
  Query: 
    users: async () => 
      const userLoader = new Dataloader(userIds => 
        const users = await User.where("_id").in(userIds)
        const usersMap = new Map(users.map(user => [user.id, user]))
        return userIds.map(userId => usersMap.get(userId))
      )
      const userIds = await User.find().distinct("_id")
      return userLoader.load(userIds)
    
  

但是,userLoader 现在在 Mongoose 模式的 friends 方法中是 undefined。那么让我们在解析器中移动架构!

// GraphQL resolver
const resolver = 
  Query: 
    users: async () => 
      const userLoader = new Dataloader(userIds => 
        const users = await User.where("_id").in(userIds)
        const usersMap = new Map(users.map(user => [user.id, user]))
        return userIds.map(userId => usersMap.get(userId))
      )
      const userSchema = new Schema(
        name: String,
        friendIds: [String]
      )
      userSchema.methods.friends = function() 
        return userLoader.load(this.friendIds)
      
      const User = mongoose.model("User", userSchema)
      const userIds = await User.find().distinct("_id")
      return userLoader.load(userIds)
    
  

Mh ... 现在 Mongoose 抱怨第二个请求:解析器再次被调用,Mongoose 不喜欢使用相同的模型名称定义 2 个模型。

“虚拟填充”功能没有用,因为我什至不能告诉 Mongoose 通过数据加载器而不是直接通过数据库来获取模型。

问题

有人遇到过同样的问题吗?有人对如何结合使用 Mongoose 和 Dataloader 有建议吗?谢谢。

注意:我知道由于我的架构是“关系型”,我应该使用关系型数据库而不是 MongoDB。我不是做出那个选择的人。在我们可以迁移之前,我必须忍受它。

【问题讨论】:

【参考方案1】:

将猫鼬模式保存在单独的模块中。您不想在每次请求时都创建架构 - 只是在第一次导入模块时。

const userSchema = new Schema(
  name: String,
  friendIds: [String]
)
const User = mongoose.model("User", userSchema)

module.exports =  User 

如果需要,您还可以导出在同一模块中创建加载程序的函数。但是请注意,我们不想导出加载器的实例,而只是导出将返回的函数。

// ...
const getUserLoader = () => new DataLoader((userIds) => 
  return User.find( _id:  $in: userIds  ).execute()
)
module.exports =  User, getUserLoader 

接下来,我们要在上下文中包含我们的加载器。具体如何完成将取决于您使用什么库来实际公开您的 graphql 端点。例如,在apollo-server 中,上下文作为配置的一部分传入。

new ApolloServer(
    typeDefs,
    resolvers,
    context: ( req ) => (
        userLoader: getUserLoader()
    ),
)

这将确保我们为每个请求创建一个新的加载器实例。现在,您的解析器可以像这样调用加载器:

const resolvers = 
  Query: 
    users: async (root, args,  userLoader ) => 
      // Our loader can't get all users, so let's use the model directly here
      const allUsers = await User.find()
      // then tell the loader about the users we found
      for (const user of allUsers) 
        userLoader.prime(user.id, user);
      
      // and finally return the result
      return allUsers
    
  ,
  User: 
    friends: async (user, args,  userLoader ) => 
      return userLoader.loadMany(user.friendIds)
    ,
  ,

【讨论】:

感谢您的详细回答。我不知道您可以按照您为用户建议的方式在类型上添加解析器。但是,我还需要模型中其他地方的 friends 方法:仅在 GraphQL 中公开它是不够的。它违背了使用 ORM 的目的。 这个方法还是可以的。更大的一点是您的模型不应维护 Dataloader 实例。应该为每个 GraphQL 请求、每个脚本执行以及代码在其中执行的任何上下文创建一个新实例 这一点我很清楚,如果我的问题没有很好地提出,请见谅。我可以尝试改进它。我试着让它变得简单。我的真实模型比这里介绍的要复杂得多。我将有一个deletedFriends 方法、一个intainedFriends 方法、一个closeFriends 方法……每个都通过向查询添加条件来改进friends 方法。您是否会建议使用每种方法两次,一次在我的模型中,一次在 GraphQL 模式中?这似乎非常冗长/不干燥。我还想在脚本执行中使用我的模型,前面没有 GraphQL,并且仍然从 DataLoader 中受益......

以上是关于如何将 Mongoose 与 GraphQL 和 DataLoader 一起使用?的主要内容,如果未能解决你的问题,请参考以下文章

GraphQL & Mongoose Schema - 如何将一组 mongoose objectId 引用存储到另一种类型?

Mongoose 和 GraphQL 查询

如何使用 graphql 和 mongoose 进行突变

GraphQL、Relay 和 Mongodb (mongoose) 如何获取数组

graphql + mongoose + typescript,如何减少重复的模型定义

返回突变结果的正确方法? (GraphQL 与 Apollo 和 Mongoose