我有非常常见的 GraphQL 模式,如下所示(伪代码):
Post {
commentsPage(skip: Int, limit: Int) {
total: Int
items: [Comment]
}
}
因此,为了避免在请求多个Post
对象时出现 n+1 问题,我决定使用 Facebook 的 Dataloader。
由于我正在开发 Nest.JS 3 层分层应用程序(解析器-服务-存储库),我有问题:
我应该用 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 一起使用而不是与服务一起使用)?
免责声明:我不是 Nest 的专家.js但我写了很多数据加载器,并使用自动生成的数据加载器。尽管如此,我希望我能给出一些见解。
实际问题是什么?
虽然你的问题似乎是一个相对简单的问题,但它可能比这困难得多。我认为实际问题如下:是否对特定字段使用数据加载器模式需要根据每个字段来决定。另一方面,存储库+服务模式试图通过公开抽象而强大的数据访问方式来抽象出这一决定。一种出路是简单地"数据加载器化"服务的每种方法。不幸的是,在实践中,这并不可行。让我们来探索一下原因!
数据加载器用于键值查找
数据加载器提供了一个承诺缓存,以减少对数据库的重复调用。要使此缓存正常工作,所有请求都需要是简单的键值查找(例如userByIdLoader
,postsByUserIdLoader
)。这很快就会变得不够,就像在您的一个示例中,您对存储库的请求有很多参数:
this.repository.find(postId, dateStart, dateEnd, skip, limit);
当然,从技术上讲,您可以制作{ postId, dateStart, dateEnd, skip, limit }
密钥,然后以某种方式对内容进行哈希处理以生成唯一密钥。
编写数据加载器查询比普通查询困难一个数量级
当您实现数据加载器查询时,它现在突然必须为初始查询所需的输入列表工作。这里有一个简单的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 查找加载。Twitter 上的这个话题讨论了 GraphQL API 应该如何只公开可以有效查询的内容。如果您使用强大的过滤器方法创建服务方法,即使您的 GraphQL API 没有公开这些过滤器,您也会遇到同样的问题。
好的,那么解决方案是什么?
据我了解,Facebook做的第一件事就是非常紧密地匹配字段和服务方法。你也可以这样做。这样,您就可以在服务方法中决定是否要使用数据加载器。例如,我不在根查询中使用数据加载器(例如{ getPosts(filter: { createdBefore: "...", user: 234 }) { .. }
),但在列表{ getAllPosts { comments { ... } }
中显示的类型的子字段中。根查询不会在循环中执行,因此不会暴露给 n+1 问题。
您的存储库现在公开了可以"有效查询"的内容(如 Lee 的推文),例如外键/主键查找或过滤查找所有查询。然后,该服务可以将密钥查找包装在数据加载器中。通常,我最终会在业务逻辑中过滤小列表。我认为这对于小型应用程序来说完全没问题,但在扩展时可能会出现问题。JavaScript 的 GraphQL Relay 助手在使用connectionFromArray
函数时会执行类似的操作。分页不是在数据库级别完成的,这对于 90% 的连接来说可能是可以的。
需要考虑的一些来源
- GraphQL 在 GraphQL 之前 - Dan Schafer
- 数据加载器源代码演练 - 李·拜伦
- 今年 GraphQL 会议还有另一个讨论 FB 的数据访问,但我认为它还没有上传。我可能会在它出版后回来。