【问题标题】:Should GraphQL DataLoader wrap request to database or wrap requests to service methods?GraphQL DataLoader 应该将请求包装到数据库还是将请求包装到服务方法?
【发布时间】:2019-12-03 19:03:40
【问题描述】:

我有这样的非常常见的 GraphQL 架构(伪代码):

Post {
  commentsPage(skip: Int, limit: Int) {
    total: Int
    items: [Comment]
  }
}

为了避免在请求多个Post 对象时出现n+1 问题,我决定使用Facebook 的Dataloader。

由于我正在开发 Nest.JS 3 层分层应用程序(Resolver-Service-Repository),我有一个问题:

我应该使用 DataLoader 包装我的存储库方法还是应该使用 Dataloder 包装我的服务方法?

下面是我的服务方法示例,它返回Comments 页面(即从commentsPage 属性解析器调用此方法)。在服务方法内部,我使用了 2 个存储库方法(#count#find):

@Injectable()
export class CommentsService {
    constructor(
        private readonly repository: CommentsRepository,
    ) {}

    async getCommentsPage(postId, dataStart, dateEnd, skip, limit): PaginatedComments {
        const counts = await this.repository.getCount(postId, dateStart, dateEnd);
        const itemsDocs = await this.repository.find(postId, dateStart, dateEnd, skip, limit);
        const items = this.mapDbResultToGraphQlType(itemsDocs);
        return new PaginatedComments(total, items)
    }
}

所以我应该为每个存储库方法(#count#find 等)创建单独的 Dataloader 实例,还是应该只用 Dataloader 包装我的整个服务方法(所以我的 commentsPage 属性解析器只能与 Dataloader 一起使用没有服务)?

【问题讨论】:

    标签: graphql nestjs dataloader


    【解决方案1】:

    免责声明:我不是 Nest.js 方面的专家,但我编写了很多数据加载器,并且使用过自动生成的数据加载器。尽管如此,我希望我能提供一些见解。

    真正的问题是什么?

    虽然您的问题似乎是一个相对简单的非此即彼的问题,但可能比这要困难得多。我认为实际问题如下:是否对特定字段使用数据加载器模式需要根据每个字段来决定。另一方面,存储库+服务模式试图通过公开抽象而强大的数据访问方式来抽象出这个决定。一种出路是简单地“dataloaderify”您服务的每种方法。不幸的是,在实践中这并不是真正可行的。让我们来探究一下原因!

    Dataloader 用于键值查找

    Dataloader 提供了一个承诺缓存,以减少对数据库的重复调用。为了让这个缓存工作,所有请求都需要简单的键值查找(例如userByIdLoaderpostsByUserIdLoader)。这很快就变得不够了,就像在您的一个示例中,您对存储库的请求有很多参数:

    this.repository.find(postId, dateStart, dateEnd, skip, limit);
    

    当然,从技术上讲,您可以将 { postId, dateStart, dateEnd, skip, limit } 设为您的密钥,然后以某种方式对内容进行哈希处理以生成唯一密钥。

    编写 Dataloader 查询比普通查询困难一个数量级

    当您实现数据加载器查询时,它现在突然必须处理初始查询所需的输入列表。这里是一个简单的 SQL 示例:

    SELECT * FROM user WHERE id = ?
    -- Dataloaded
    SELECT * FROM user WHERE id IN ?
    

    好的,现在是上面的存储库示例:

    SELECT * FROM comment WHERE post_id = ? AND date < ? AND date > ? OFFSET ? LIMIT ?
    -- Dataloaded
    ???
    

    我有时会编写适用于两个参数的查询,它们已经成为非常困难的问题。这就是为什么大多数数据加载器只是 按 id 加载 查找。 This tread on twitter 讨论了 GraphQL API 如何只公开可以有效查询的内容。如果您使用强大的过滤器方法创建服务方法,即使您的 GraphQL API 没有公开这些过滤器,您也会遇到同样的问题。

    好的,那么解决方法是什么?

    据我了解,Facebook 所做的第一件事就是非常紧密地匹配字段和服务方法。你也可以这样做。这样,您可以在服务方法中做出决定是否要使用数据加载器。例如,我不在根查询(例如{ getPosts(filter: { createdBefore: "...", user: 234 }) { .. })中使用数据加载器,而是在列表{ getAllPosts { comments { ... } } 中出现的类型的子字段中使用。根查询不会循环执行,因此不会遇到 n+1 问题。

    您的存储库现在公开了可以“有效查询”的内容(如 Lee 的推文中所示),例如 外键/主键查找过滤的全部查找 查询。然后,该服务可以将例如密钥查找包装在数据加载器中。通常我最终会在我的业务逻辑中过滤小列表。我认为这对于小型应用程序来说非常好,但在扩展时可能会出现问题。当您使用 connectionFromArray 函数时,用于 JavaScript 的 GraphQL Relay 助手会执行类似的操作。分页不是在数据库级别完成的,这对于 90% 的连接来说可能是可以的。

    需要考虑的一些来源

    【讨论】:

    • 哇,这是一个了不起的答案。在回答最初的问题时,您还回答了我的其他一些问题,并且解释得很清楚。谢谢!
    • 我使用“创建具有多个参数的哈希并查询所有参数”方法,如您所说。就像你说的那样,写他们的查询可能会失控。如果你有简单的验证或一些错误处理步骤,它真的失去了它的价值。对于这种情况我能做些什么,我仍然没有完全的答案。但是你的回答有很多优点。谢谢!
    猜你喜欢
    • 1970-01-01
    • 2021-08-13
    • 2020-07-05
    • 2020-07-30
    • 2012-01-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-04-07
    相关资源
    最近更新 更多