说你要查询 movies 列表,并且每个 movie 包含一些 director 数据。设想一下,Movie 和 Director 实体需要两个不同的服务。在一个纯粹的实现中,为了加载50部影片,那么你需要调用 Director 服务 50 次:每次一个 movie。一共 51 个查询:1个查询是获取 movies 列表,50个查询是为了获取每个 movie 的 director 数据。这很明显不会有很好的性能。
创建一个可以通过一次查询获取所有 directors 列表的查询更有效率。首先 Director 服务必须支持这样的查询,因为服务应该提供获取 Directors 列表的方法。那么在 Movie 服务的 data fetcher 需要更加的智能,去处理 Directors 服务的批量请求。
Data loader 负责为一个给定的 Key 列表加载数据。在这个例子中,它只传递了一个Key 的列表给拥有的 Director 的后端(这可能是一个【GRPC】服务)。然而,你也需要写一个可以从数据库中加载数据的服务。尽管这个例子注册了一个 data loader,除非你去实现一个 data fetcher 去使用它,否则没人会去用它。
BatchLoader 接口为一个 Key 列表创建了一个 Value 列表。你也可以为一个值的 Set 来使用创建了 Key/Value Map 的 MappedBatchLoader 。如果你不期望所有的 Key 都有一个值对应的话,后者提供了一个更好的选择。就像你注册一个 BatchLoader 那样,注册一个 MappedBatchLoader。
使用一个 Data Loader
以下是一个使用数据加载器(Data Loader)加载数据的例子:
上面的代码仅仅是一个普通的 data fetcher。然而,使用 data loader 来替换真正从其他服务或者数据库中获取数据。你可以从 DataFetchingEnvironment 里的 getDataLoader() 方法获取一个 data loader。这需要你传入一个 String 类型的 data loader 名称。另一个对 data fetcher 的变更是用 CompletableFuture 来代替原来实际的返回类型。这将会让框架异步工作,并且是批量处理的一个必须条件。
同样的方式也适用于将 @DgsDataLoader 当做 Lambda 来代替一个 Class 来定义。如果你在同一个 Class 中定义多个 @DgsDataLoader lambda,你不能使用这个特性。建议你通过一个 String 形式的名字来使用 getDataLoader() 的方式实现。
注意这里没有关于批处理是怎样工作的逻辑;这些都被框架处理了!框架将组织在加载多个 movie 的同时,加载多个 directors,为 data loader 处理所有的请求,并使用一个 ID list 代替单个 ID 调用 data loader。data loader 已经实现了如何处理 ID list 的方法,避免了 N+1 问题。
在 CompletableFuture 中使用 SecurityContextHolder 那样的 Spring 特性
当你写异步 data fetcher 时,代码将会执行在 worker 线程。Spring 内部存储一些 context,例如可以让 SecurityContextHolder 工作的线程。运行在不同线程的的 context 中的内部代码不可用,这使得获取与 request 相关的 Principal 将无法工作。
Spring Boot 对这有一个解决方式:它管理了一个线程池,可以处理这个 context。你可以用以下方法来注入使用:
典型的让 data fetcher 变成异步的方法是 supplyAsync(),你必须将这个 executor 当做第二个参数,传递给这个方法。
缓存
批处理是一个避免 N+1 问题的最重要的一方面。然而 Data loader 也支持缓存。如果加载同一个 Key 多次的话,应该只加载一次。比如,如果已经加载一个 movies 列表,并且一些 movies 有相同的 director,那么 director 数据应该只加载一次。
在 DGS 1 中缓存是默认关闭的
第1版的 DGS 框架默认关闭了缓存,但你可以通过 @DgsDataLoader 注解打开:
在 DGS 框架版本2中,默认开启了缓存,你并不需要修改上面的配置。
Batch Size
有时候你可能要一次性加载多个元素,但需要限制容量。比如从数据库中加载数据,在 SQL 中使用了 IN 关键字,但是可能需要限制 ID 的最大个数。 @DgsDataLoader 里有一个 maxBatchSize 的属性,你可以配置这个行为。默认是没有最大限制的。