使用Blaze Persistence+EntityViews分页查询返回重复数据的解决方法
问题
在将Blaze Persistence API与Spring Data JPA的Specification API结合使用时,通过EntityViewSpecificationExecutor搭配Pageable查询数据库返回了重复数据。场景是按语言过滤课程别名,一门课程可对应同一语言的多个别名,导致分页查询时出现重复的课程记录。
使用的技术版本:SpringBoot 3.3.3、Blaze Persistence 1.6.12、Hibernate 6。
实体定义
Course实体
@Data @Entity @Table(name = "courses") public class Course implements Serializable { @Id @Column(name = "course_id") private Long id; @Column(name = "course_name") private String courseName; @OneToMany(mappedBy = "anCourse", cascade = CascadeType.ALL) private List<AltName> altNameList; // 其他字段和关联关系已省略 }
AltName实体
@Data @Entity @Table(name = "alt_names") public class AltName implements Serializable { @Id @Column(name = "alt_id") private Long id; @Column(name = "alt_name") private String altName; @Column(name = "alt_language") String altNameLanguage; // 同一门课程可拥有同一语言的多个别名 @ManyToOne private Course anCourse; // 其他多个字段已省略 }
实体视图定义
SearchResultView
@EntityView(Course.class) public interface SearchResultView extends CourseView { @Mapping(fetch = FetchStrategy.MULTISET) public List<AltNameView> getAltNameList(); // 其他多集合映射已省略 }
CourseView
@EntityView(CourseView.class) public interface CourseView { @IdMapping public Long getCourseId(); public String getCourseName(); }
AltNameView
@EntityView(AltName.class) public interface AltNameView { @IdMapping public int getId(); public String getAltName(); public String getAltNameLanguage(); // 其他字段已省略 }
查询实现
Repository接口
@Transactional(readOnly = true) public interface CourseBlazeRepository extends EntityViewRepository<SearchResultView, Long>, EntityViewSpecificationExecutor<SearchResultView, Course> { Page<SearchResultView> findCourses(Specification<Course> courseSpecification, Pageable pageable); }
过滤Specification
// 以下方法用于查找零个、一个或多个西班牙语课程别名 public static Specification<Course> filterForSpanishLanguage(){ return ((root, criteriaQuery, criteriaBuilder) -> { Join<Course, AltName> altNames = root.join("altNameList", JoinType.INNER); return criteriaBuilder.equal(altNames.get(AltName_.ALT_NAME_LANGUAGE), "Spanish"); }); }
注意:使用原生JpaSpecificationExecutor时,可通过criteriaQuery.distinct(true)去重,但在EntityViewSpecificationExecutor中使用该语句会抛出运行时异常“Cannot paginate a DISTINCT query”,因为Blaze生成的查询已包含distinct关键字。
解决方案
1. 改用Blaze Persistence的EntityViewSetting配置去重
修改Repository的查询方法,添加EntityViewSetting参数:
Page<SearchResultView> findCourses(Specification<Course> courseSpecification, EntityViewSetting<SearchResultView> setting, Pageable pageable);
调用时显式配置去重:
EntityViewSetting<SearchResultView> setting = EntityViewSetting.create(SearchResultView.class) .distinct(true); Page<SearchResultView> result = courseBlazeRepository.findCourses(filterForSpanishLanguage(), setting, pageable);
2. 在Specification中使用EXISTS子查询替代关联查询
通过EXISTS子查询过滤符合条件的课程,避免关联查询导致的结果集膨胀:
public static Specification<Course> filterForSpanishLanguage(){ return ((root, criteriaQuery, criteriaBuilder) -> { Subquery<Long> subquery = criteriaQuery.subquery(Long.class); Root<AltName> altNameRoot = subquery.from(AltName.class); subquery.select(altNameRoot.get("anCourse").get("id")) .where( criteriaBuilder.equal(altNameRoot.get("altNameLanguage"), "Spanish"), criteriaBuilder.equal(altNameRoot.get("anCourse").get("id"), root.get("id")) ); return criteriaBuilder.exists(subquery); }); }
这种方式不会产生关联后的重复行,从根源上避免重复数据,同时兼容分页逻辑。
3. 配置Blaze Persistence的分页策略
在application.properties中添加配置,让分页基于根实体ID进行:
blaze.persistence.view.page-strategy=id
该配置会让Blaze先查询去重后的根实体ID列表,再根据ID查询完整视图数据,彻底避免重复分页结果。
内容的提问来源于stack exchange,提问作者stretch-a-lot

