JPA Specification重复调用报错及分页计数查询优化问题
问题与解答
问题描述
我基于Carl Mapada《高级搜索与过滤》的代码做了大量修改,原本运行正常,添加Pageable分页功能后,遇到了Specification结合Pageable的计数查询问题。于是我编写了CustomRepository并重写readPage方法跳过计数,分页功能恢复正常,但要显示“1-100,共1482条结果”这类信息,需要单独做计数查询(当前分页返回的总数只是当前页的数量)。
为实现动态查询的总计数,我尝试多种方法都没成功,过程中发现一个奇怪的问题:服务层两次调用仓库方法传入同一Specification实例时会报错(即使不带分页):
MyEntitySpecBuilder builder = new MyEntitySpecBuilder(filter, specs); Specification<MyEntity> spec = builder.build(); List<MyEntity> entities1 = myEntityRepository.findAll(spec); List<MyEntity> entities2 = myEntityRepository.findAll(spec);
第二次调用findAll会抛出JpaSystemException:无法在blah.blah.EntityClassAttribute上找到名为xyz的属性。
但每次调用重新构建Specification实例就不会报错:
MyEntitySpecBuilder builder = new MyEntitySpecBuilder(filter, specs); Specification<MyEntity> spec = builder.build(); List<MyEntity> entities1 = myEntityRepository.findAll(spec); List<MyEntity> entities2 = myEntityRepository.findAll(builder.build());
我原本想复用同一Specification实例实现计数与分页查询,现在有两个问题:
- 在多条件Specification构建查询的场景下(如《高级搜索与过滤》示例),若需获取总计数,是否有推荐实现方式?最好在CustomRepository中实现而非Service层,我设想在自定义仓库方法中传入SpecificationBuilder,分别构建两个实例用于计数和结果查询。
- 核心疑问:Specification实例首次使用后是否会被修改?为何重复使用同一实例报错,不同实例则正常?
解答
1. 多条件Specification场景下获取总计数的推荐实现
推荐在CustomRepository中封装逻辑,避免在Service层处理重复构建的细节:
- 自定义仓库方法,直接接收构建Specification所需的原始参数(比如
filter和specs),而非传入已构建好的Specification实例。在方法内部两次调用builder.build()生成独立的Specification对象,分别用于执行计数查询(count(spec))和分页查询(findAll(spec, pageable))。 - 可以返回自定义结果对象,包含分页数据列表和总计数;或者手动构造Spring Data的
Page对象(通过PageImpl),把查询到的列表、分页参数、总计数传入,上层可直接用Spring的Page类型处理。 - 这种方式既符合你希望在Repository层实现的需求,又能避免复用Specification实例带来的问题。
2. 为何重复使用同一Specification实例会报错
Specification实例首次使用后会被JPA查询框架修改内部状态,原因如下:
- 基于Carl Mapada示例的自定义Specification实现,通常会在构建过程中持有
CriteriaBuilder、Root等和查询上下文绑定的对象引用。这些对象仅和单次查询绑定,第一次查询结束后,对应的上下文已失效。 - 当你第二次复用同一个Specification实例时,它内部存储的还是第一次查询的上下文信息,在新的查询中,这些信息无法适配新的
CriteriaQuery环境,就会出现找不到属性的错误。 - 简单来说,你的Specification实例不是无状态的,第一次使用后内部已被污染,必须每次查询都构建新的实例,保证每个实例都是干净、独立的查询条件载体。
内容的提问来源于stack exchange,提问作者user2052618
相关产品推荐
相关产品推荐

