Blaze Persistence单表继承下分页关联查询优化求助
问题背景
- 技术栈:Spring Boot + Blaze Persistence
- 实体结构:
Animal为基类,Pet继承自Animal,采用单表继承策略 - 现有分页关联查询方案:分两次DB调用——先分页查询
Animal的ID列表,再通过ID批量查询带关联(Pet.behaviors、Pet.species)的实体,最后转换为DTO返回Page对象 - 优化需求:将DB调用次数减少到最多2次(优先1次完成分页+关联数据获取)
尝试过的方案及问题
方案1:直接关联查询加distinct
尝试直接在关联查询中加入分页参数和distinct,但抛出java.sql.SQLSyntaxErrorException: Duplicate column name...异常,代码如下:
@Transactional(readOnly = true) public Page<AnimalDto> findAll(Pageable pageable, SpecSearchCriteria searchCriteria) { CriteriaBuilder<Animal> cb = criteriaBuilderFactory.create(entityManager, Animal.class) .distinct() .from(Animal.class, "animal") .leftJoin("TREAT(animal AS Pet).behaviors", "behav") .leftJoin("TREAT(animal AS Pet).species", "spec") .fetch("behav") .fetch("spec") .setFirstResult((int) pageable.getOffset()) .setMaxResults(pageable.getPageSize()); List<Animal> animals = cb.getResultList(); Long totalCount = cb.getCountQuery().getSingleResult(); List<AnimalDto> dtos = animals.stream() .map(animal -> animalOperations.get(animal.getClass().getSimpleName()).mapToDto(animal)) .toList(); return new PageImpl<>(dtos, pageable, totalCount); }
方案2:使用Blaze Persistence PaginatedCriteriaBuilder
使用分页构建器后出现两个问题:
- 分页总元素数统计不正确
- 查询结果出现重复记录
代码如下:
@Transactional(readOnly = true) public Page<AnimalDto> findAll(Pageable pageable, SpecSearchCriteria searchCriteria) { CriteriaBuilder<Animal> cb = criteriaBuilderFactory.create(entityManager, Animal.class) .from(Animal.class, "a") .fetch("TREAT(a AS Pet).behaviors") .fetch("TREAT(a AS Pet).species") .orderByAsc("a.id"); List<Animal> animals = cb .page(pageable.getPageNumber(), pageable.getPageSize()) .withCountQuery(false) .getResultList(); CriteriaBuilder<Long> countCb = criteriaBuilderFactory.create(entityManager, Long.class) .from(Animal.class, "a") .select("COUNT(DISTINCT a.id)"); Long totalCount = countCb.getSingleResult(); List<AnimalDto> dtos = animals.stream() .map(animal -> animalOperations.get(animal.getClass().getSimpleName()).mapToDto(animal)) .toList(); return new PageImpl<>(dtos, pageable, totalCount); }
优化后的解决方案
针对单表继承的关联分页场景,Blaze Persistence需要正确处理DISTINCT和关联fetch的关系,同时确保分页计数的准确性。以下是符合需求的实现(仅2次DB调用:一次分页查询实体+关联,一次计数查询):
@Transactional(readOnly = true) public Page<AnimalDto> findAll(Pageable pageable, SpecSearchCriteria searchCriteria) { // 1. 构建带关联fetch的分页查询,使用DISTINCT避免笛卡尔积导致的重复实体 CriteriaBuilder<Animal> fetchCb = criteriaBuilderFactory.create(entityManager, Animal.class) .distinct() .from(Animal.class, "a") // 针对单表继承的子类关联,直接使用TREAT语法fetch,无需额外join .fetch("TREAT(a AS Pet).behaviors", JoinType.LEFT) .fetch("TREAT(a AS Pet).species", JoinType.LEFT) .orderByAsc("a.id") .setFirstResult((int) pageable.getOffset()) .setMaxResults(pageable.getPageSize()); // 应用搜索条件(与业务逻辑同步) if (searchCriteria != null) { // 此处添加搜索条件转换逻辑,例如: // fetchCb.where("a.name").like(searchCriteria.getKeyword()) } List<Animal> animals = fetchCb.getResultList(); // 2. 构建正确的计数查询,使用DISTINCT统计主实体数量 CriteriaBuilder<Long> countCb = criteriaBuilderFactory.create(entityManager, Long.class) .from(Animal.class, "a") .select("COUNT(DISTINCT a.id)"); // 同步应用搜索条件,保证计数与查询结果一致 if (searchCriteria != null) { // 此处添加与分页查询完全一致的搜索条件 // countCb.where("a.name").like(searchCriteria.getKeyword()) } Long totalCount = countCb.getSingleResult(); // 转换为DTO List<AnimalDto> dtos = animals.stream() .map(animal -> animalOperations.get(animal.getClass().getSimpleName()).mapToDto(animal)) .toList(); return new PageImpl<>(dtos, pageable, totalCount); }
关键优化点说明
- 避免重复列错误:直接使用
fetch("TREAT(a AS Pet).xxx", JoinType.LEFT)替代先leftJoin再fetch的写法,Blaze Persistence会自动处理关联的别名和列映射,避免手动join导致的重复列问题 - 解决重复记录:必须添加
.distinct(),因为多关联fetch会产生笛卡尔积,Blaze Persistence会基于实体ID去重,返回唯一的Animal实例(关联集合会正确填充) - 准确计数:计数查询必须使用
COUNT(DISTINCT a.id),避免笛卡尔积导致的计数偏大;同时要确保计数查询的过滤条件和分页查询完全一致,保证总条数准确 - 单表继承兼容:
TREAT语法是JPA标准的类型转换方式,Blaze Persistence对其有良好支持,能正确识别单表继承中的子类关联属性
内容的提问来源于stack exchange,提问作者rechman linjie
相关产品推荐
相关产品推荐

