N+1问题介绍

对于上一篇文章样例,如果要获取每个电影的演员名单,要执行如下动作

  • 查询所有电影清单
  • 遍历N个电影,查询对应电影的演员名单

总查询开销为N+1次查询,代价非常大,效率低

优化方案 DataLoader

对于一对一表关联的情况且每个关联对象只有一个值,可以直接使用BatchLoader

但实际上,对于大部分表关联情况,通常为一对多或者多对多,不保证每个关联都有值,此时需要使用MappedBatchLoader维护关联关系

维护多对多实体关系Map

public interface ActorService {
	Map> listByFilmId(Collection filmIds);
}
@Slf4j
@Service
@RequiredArgsConstructor
public class ActorServiceImpl implements ActorService {

	private final ActorRepository actorRepository;

	private final FilmActorRepository filmActorRepository;

	@Override
	public Map> listByFilmId(Collection filmIds) {
		List filmActors = filmActorRepository.listByFilmId(filmIds);
		if (CollectionUtils.isEmpty(filmActors)) {
			return Collections.emptyMap();
		}
		Map> filmActorMap = filmActors.stream().collect(Collectors.groupingBy(FilmActor::getFilmId));
		List actorIds = filmActors.stream().map(FilmActor::getActorId).distinct().toList();
		List actors = actorRepository.listByIds(actorIds);
		Map actorsMap = Stream.ofNullable(actors).flatMap(Collection::stream).collect(Collectors.toMap(Actor::getActorId, Function.identity(), (e1, e2) -> e1));
		Map> result = Maps.newHashMapWithExpectedSize(filmIds.size());
		filmActorMap.forEach((k, v) -> {
			List actorList = Stream.ofNullable(filmActorMap.get(k)).flatMap(Collection::stream).map(e -> actorsMap.get(e.getActorId())).toList();
			result.put(k, actorList);
		});
		return result;
	}
}

Dgs MappedBatchLoader 配置

@DgsDataLoader(name = "actors")
@RequiredArgsConstructor
public class FilmActorsDataLoader implements MappedBatchLoader> {

	private final ActorService actorService;

	@Override
	public CompletionStage>> load(Set keys) {
		return CompletableFuture.supplyAsync(() -> actorService.listByFilmId(keys));
	}
}

测试

访问http://localhost:8080/graphiql即可看到在线查询页面

image-20221027223715143

此时执行嵌套查询只会查询2次,一次查电影列表,一次查所有电影的Actor列表,非常快速

image-20221027223756097

总结

使用BatchLoader实现对N+1问题优化,但还有一个潜在问题,即大数据分页,该样例中,有1000个电影,对应5000+演员,查询一次获取全量数据,对数据库压力较大,也不符合实际场景,还需要进行分页优化

最后修改:2022 年 10 月 28 日
如果觉得我的文章对你有用,请随意赞赏