为SpringDataJpa集成QueryObject模式
itomcoil 2025-07-06 12:58 2 浏览
1.概览
单表查询在业务开发中占比最大,是所有 CRUD Boy 的入门必备,所有人在 JavaBean 和 SQL 之间乐此不疲。而在我看来,该部分是最枯燥、最没有技术含量的“伪技能”。
1.1. 背景
针对单表查询的 JPA 封装,很多读者反馈很方便也很简单,确实能解决了不少问题:
- 不需要写 SQL,能够快速实现,提升开发效率
- 避免查询条件不当引起的性能问题
但,有眼光锐利的读者提出一个问题:为什么要在 SpringData Repository 之外定义一个新的 Repository,而不是与 Spring Data 集成呢?
这是一个非常好的问题,本次我们就解决与 Spring Data 集成问题。
1.2. 目标
实现 QueryObjectRepository 与 Spring Data Jpa 的集成,无需实现新的 Repository,只需按 spring data 规范完成接口定义,由框架生成的 proxy 实现所有的逻辑。
2. 快速入门
2.1. 环境搭建
2.1.1. 引入 spring-data-jpa
首先,引入 Spring data jpa 相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
其次,新建 JpaUser Entity 类:
@Data
@Entity
@Table(name = "t_user")
public class JpaUser implements User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer status;
private Date birthAt;
private String mobile;
}
新建 JpaUserRepository
public interface JpaUserRepository extends Repository<JpaUser, Long>, JpaSpecificationExecutor<JpaUser> {
}
JpaUserRepository 继承两个接口:
- Repository,标记为一个仓库,由 spring data 为其创建代理类;
- JpaSpecificationExecutor,使其具备 Specification 查询能力;
2.1.2. 引入 singlequery
在 pom 中增加 singlequery 相关依赖:
<dependency>
<groupId>com.geekhalo.lego</groupId>
<artifactId>lego-starter-singlequery</artifactId>
<version>0.1.7-query-SNAPSHOT</version>
</dependency>
Starter 中的
JpaBasedSingleQueryConfiguration 将为我们完成全部配置。
2.2. 【旧】自定义 QueryObjectRepository 方案
接触过旧版本的读者,可以跳过,直接看下一章“spring data jpa 集成”
2.2.1. 定义 Repository
创建 JpaUserSingleQueryService,继承自
BaseSpecificationQueryObjectRepository,具体如下:
@Repository
public class JpaUserSingleQueryService
extends BaseSpecificationQueryObjectRepository
implements UserSingleQueryService {
public JpaUserSingleQueryService(JpaUserRepository specificationExecutor) {
super(specificationExecutor, JpaUser.class);
}
}
其中,构造参数 JpaUserRepository 为 spring data jpa 为我们生成的 Proxy;
BaseSpecificationQueryObjectRepository 为我们提供基本的查询能力;
2.2.2. 创建查询对象,添加查询注解
定义查询对象,具体如下:
@Data
public class QueryByIdIn {
@FieldIn(value = "id", fieldType = Long.class)
private List<Long> ids;
}
其中,@FieldIn 表明过滤字段和过滤方式;
2.2.3. 运行单元测试
编写测试用例如下:
@Test
void getByIds() {
List<Long> ids = Arrays.asList(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L);
QueryByIdIn queryByIdIn = new QueryByIdIn();
queryByIdIn.setIds(ids);
{
List<User> users = this.getSingleQueryService().listOf(queryByIdIn);
Assertions.assertNotNull(users);
Assertions.assertTrue(CollectionUtils.isNotEmpty(users));
Assertions.assertEquals(10, users.size());
}
{
Long count = this.getSingleQueryService().countOf(queryByIdIn);
Assertions.assertEquals(10L, count);
}
}
运行用例,控制台打印 SQL 如下:
Hibernate: select
jpauser0_.id as id1_0_, jpauser0_.birth_at as birth_at2_0_, jpauser0_.mobile as mobile3_0_, jpauser0_.name as name4_0_, jpauser0_.status as status5_0_
from t_user jpauser0_
where jpauser0_.id in (? , ? , ? , ? , ? , ? , ? , ? , ? , ?)
Hibernate: select
count(jpauser0_.id) as col_0_0_
from t_user jpauser0_
where jpauser0_.id in (? , ? , ? , ? , ? , ? , ? , ? , ? , ?)
当前支持的过滤注解包括:
注解 | 含义 |
FieldEqualTo | 等于 |
FieldGreaterThan | 大于 |
FieldGreaterThanOrEqualTo | 大于等于 |
FieldIn | in 操作 |
FieldIsNull | 是否为 null |
FieldLessThan | 小于 |
FieldLessThanOrEqualTo | 小于等于 |
FieldNotEqualTo | 不等于 |
FieldNotIn | not in |
EmbeddedFilter | 嵌入查询对象 |
2.2.4. 嵌入对象查询
新建 嵌入对象 QueryByStatusAndBirth,在类上增加过滤注解,具体如下:
@Data
public class QueryByStatusAndBirth {
@FieldEqualTo("status")
private Integer status;
@FieldGreaterThan("birthAt")
private Date birthAfter;
}
新建查询对象 QueryByEmbeddedFilter,使用 @EmbeddedFilter 标注嵌入对象,具体如下:
@Data
public class QueryByEmbeddedFilter {
@FieldGreaterThan("id")
private Long id;
@EmbeddedFilter
private QueryByStatusAndBirth statusAndBirth;
}
编写测试用例:
@Test
void queryByEmbeddedFilter() throws Exception{
QueryByEmbeddedFilter query = new QueryByEmbeddedFilter();
query.setId(0L);
QueryByStatusAndBirth queryByStatusAndBirth = new QueryByStatusAndBirth();
query.setStatusAndBirth(queryByStatusAndBirth);
queryByStatusAndBirth.setStatus(1);
queryByStatusAndBirth.setBirthAfter(DateUtils.parseDate("2018-10-01", "yyyy-MM-dd"));
List<User> users = getSingleQueryService().listOf(query);
Assertions.assertTrue(CollectionUtils.isNotEmpty(users));
}
运行测试,获取如下结果:
Hibernate: select
jpauser0_.id as id1_0_, jpauser0_.birth_at as birth_at2_0_, jpauser0_.mobile as mobile3_0_, jpauser0_.name as name4_0_, jpauser0_.status as status5_0_
from t_user jpauser0_
where jpauser0_.id>0 and jpauser0_.status=1 and jpauser0_.birth_at>?
2.2.5. 排序&分页
新建的查询对象 PageByIdGreater,具体如下:
@Data
public class PageByIdGreater {
@FieldGreaterThan("id")
private Long startId;
private Pageable pageable;
private Sort sort;
}
除过滤注解外,新增 Pageable 和 Sort 两个属性。
添加 单元测试 如下:
@Test
void pageOf(){
{
PageByIdGreater pageByIdGreater = new PageByIdGreater();
pageByIdGreater.setStartId(0L);
Pageable pageable = new Pageable();
pageByIdGreater.setPageable(pageable);
pageable.setPageNo(0);
pageable.setPageSize(5);
Sort sort = new Sort();
pageByIdGreater.setSort(sort);
Sort.Order order = Sort.Order.<Orders>builder()
.orderField(Orders.ID)
.direction(Sort.Direction.ASC)
.build();
sort.getOrders().add(order);
Page<User> userPage = this.getSingleQueryService().pageOf(pageByIdGreater);
Assertions.assertTrue(userPage.hasContent());
Assertions.assertEquals(5, userPage.getContent().size());
Assertions.assertEquals(0, userPage.getCurrentPage());
Assertions.assertEquals(5, userPage.getPageSize());
Assertions.assertEquals(3, userPage.getTotalPages());
Assertions.assertEquals(13, userPage.getTotalElements());
Assertions.assertTrue(userPage.isFirst());
Assertions.assertFalse( userPage.hasPrevious());
Assertions.assertTrue( userPage.hasNext());
Assertions.assertFalse(userPage.isLast());
}
}
运行单元测试,获取如下结果:
Hibernate: select
jpauser0_.id as id1_0_, jpauser0_.birth_at as birth_at2_0_, jpauser0_.mobile as mobile3_0_, jpauser0_.name as name4_0_, jpauser0_.status as status5_0_
from t_user jpauser0_
where jpauser0_.id>0 order by jpauser0_.id asc limit ?
Hibernate: select
count(jpauser0_.id) as col_0_0_
from t_user jpauser0_
where jpauser0_.id>0
先通过 count 查询获取总量,然后通过 limit 进行分页查询获取数据,最终将两者封装成 Page 对象。
2.2.6. 最大返回值管理
单次返回太多值是数据库性能杀手,框架通过 @MaxResult 对其进行部分支持。
目前支持包括:
策略 | 含义 |
LOG | 返回结果超过配置值后,打印日志,进行跟踪 |
ERROR | 返回结果超过配置值后,直接抛出异常 |
SET_LIMIT | 将 limit 最大值设置为 配置值,对返回值进行限制 |
新建查询对象
QueryByIdGreaterWithMaxResult,在类上增加 @MaxResult 注解,具体如下:
@Data
@MaxResult(max = 10, strategy = MaxResultCheckStrategy.LOG)
public class QueryByIdGreaterWithMaxResult {
@FieldGreaterThan(value = "id")
private Long startUserId;
}
其中,max 指最大返回值,strategy 为 日志,运行结果如下:
Hibernate: select
jpauser0_.id as id1_0_, jpauser0_.birth_at as birth_at2_0_, jpauser0_.mobile as mobile3_0_, jpauser0_.name as name4_0_, jpauser0_.status as status5_0_
from t_user jpauser0_
where jpauser0_.id>0
【LOG】result size is 13 more than 10, dao is org.springframework.data.jpa.repository.support.SimpleJpaRepository@77d959f1 param is QueryByIdGreaterWithMaxResult(startUserId=0)
将 strategy 修改为 ERROR,运行测试,抛出异常:
com.geekhalo.lego.core.singlequery.ManyResultException
at com.geekhalo.lego.core.singlequery.support.AbstractQueryRepository.processForMaxResult(AbstractQueryRepository.java:34)
at com.geekhalo.lego.core.singlequery.jpa.support.AbstractSpecificationQueryRepository.listOf(AbstractSpecificationQueryRepository.java:107)
将 strategy 修改为 SET_LIMIT,运行测试,观察 SQL,通过 limit 自动对返回值进行限制。
Hibernate: select
jpauser0_.id as id1_0_, jpauser0_.birth_at as birth_at2_0_, jpauser0_.mobile as mobile3_0_, jpauser0_.name as name4_0_, jpauser0_.status as status5_0_
from t_user jpauser0_
where jpauser0_.id>0 limit ?
【SET_LIMIT】result size is 10 more than 10, please find and fix, dao is org.springframework.data.jpa.repository.support.SimpleJpaRepository@35841d6 param is QueryByIdGreaterWithMaxResult(startUserId=0)
2.3. Spring data jpa 集成
2.3.1. 自定义 repositoryFactoryBean
首先,需要对 SimpleJpaRepository 实现进行功能扩展,并让框架实现自定义的 Repository 实现。
具体操作如下:
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true)
@EnableJpaRepositories(basePackages = {
"com.geekhalo.lego.singlequery.jpa",
"com.geekhalo.lego.validator",
"com.geekhalo.lego.query"
}, repositoryFactoryBeanClass = JpaBasedQueryObjectRepositoryFactoryBean.class)
public class DemoApplication {
public static void main(String[] args){
SpringApplication.run(DemoApplication.class, args);
}
}
通过指定 @EnableJpaRepositories 的
repositoryFactoryBeanClass 属性,使框架使用自定义的
JpaBasedQueryObjectRepository 作为 Repository 的默认实现。
2.3.2. 自定义 JpaRepository
按 spring data 标准,只需定义 Repository 接口,无需实现。具体示例如下:
public interface JpaUserRepositoryV2
extends JpaRepository<JpaUser, Long> ,
QueryObjectRepository<JpaUser> {
}
其中:
- JpaRepository 为 spring data jpa 提供的 Repository 扩展;
- QueryObjectRepository 为 QueryObject 扩展;
2.3.3. 实现效果
以最复杂的“最大返回值管理”为例,测试代码如下:
@Test
void queryByIdGreaterWithMaxResult(){
QueryByIdGreaterWithMaxResult query = new QueryByIdGreaterWithMaxResult();
query.setStartUserId(0L);
List<? extends User> users = getSingleQueryService().listOf(query);
Assertions.assertTrue(CollectionUtils.isNotEmpty(users));
}
结果如下:
Hibernate: select
jpauser0_.id as id1_2_, jpauser0_.birth_at as birth_at2_2_, jpauser0_.mobile as mobile3_2_, jpauser0_.name as name4_2_, jpauser0_.status as status5_2_
from t_user jpauser0_
where jpauser0_.id>0 limit ?
Hibernate: select
count(jpauser0_.id) as col_0_0_ from t_user jpauser0_
where jpauser0_.id>0
c.g.l.c.s.s.AbstractQueryRepository : 【SET_LIMIT】result size is 10 more than 10, please find and fix, dao is com.geekhalo.lego.core.singlequery.jpa.support.JpaBasedQueryObjectRepository@50008974 param is QueryByIdGreaterWithMaxResult(startUserId=0)
3. 小结
两者方案对比,与 Spring data 集成后,使用变得非常简单:
- 符合 Spring data 的使用风格,降低使用门槛;
- 无需定义新的 Repository 体系,避免逻辑分离;
- 最关键的是,所提供的能力没有任何减少;
4. 设计&扩展
4.1. 功能扩展
spring data jpa 具有非常强的扩展性,整体如下:
image
具体扩展点包括:
- QueryObjectRepository 定义一套基于 “查询对象” 的查询接口,包括 get、listOf、pageOf、countOf;
- SpecificationQueryObjectRepository 扩展自 QueryObjectRepository,基于 Specification 实现查询;
- JpaBasedQueryObjectRepository 实现 SpecificationQueryObjectRepository 接口,扩展自 SimpleJpaRepository,在 SimpleJpaRepository 基础上提供 SpecificationQueryObjectRepository 的通用实现;
4.2. 框架集成
image
与框架集成的核心在
JpaBasedQueryObjectRepositoryFactoryBean,也就是在 @EnableJpaRepositories 的
repositoryFactoryBeanClass 指定的类,该类主要用于为每个 Repository 提供 RepositoryFactory,有 RepositoryFactory 为其生成代理。
5. 项目信息
项目仓库地址:
https://gitee.com/litao851025/lego
项目文档地址:
https://gitee.com/litao851025/lego/wikis/support/SingleQuery-spring-data-jpa
相关推荐
- zabbix企业微信告警(zabbix5.0企业微信告警详细)
-
zabbix企业微信告警的前提是用户有企业微信且创建了一个能够发送消息的应用,具体怎么创建可以协同用户侧企业微信的管理员。第一步:企业微信准备我们需要的内容包括企业ID,应用的AgentId和应用的S...
- 基于centos7部署saltstack服务器管理自动化运维平台
-
概述SaltStack是一个服务器基础架构集中化管理平台,具备配置管理、远程执行、监控等功能,基于Python语言实现,结合轻量级消息队列(ZeroMQ)与Python第三方模块(Pyzmq、PyCr...
- 功能实用,效率提升,Python开发的自动化运维工具
-
想要高效的完成日常运维工作,不论是代码部署、应用管理还是资产信息录入,都需要一个自动化运维平台。今天我们分享一个开源项目,它可以帮助运维人员完成日常工作,提高效率,降低成本,它就是:OpsManage...
- centos定时任务之python脚本(centos7执行python脚本)
-
一、crontab的安装默认情况下,CentOS7中已经安装有crontab,如果没有安装,可以通过yum进行安装。yuminstallcrontabs二、crontab的定时语法说明*代表取...
- Fedora 41 终于要和 Python 2.7 说再见了
-
红帽工程师MiroHroncok提交了一份变更提案,建议在Fedora41中退役Python2.7,并放弃仍然依赖Python2的软件包。Python2已于2020年1...
- 软件测试|使用docker搞定 Python环境搭建
-
前言当我们在公司的电脑上搭建了一套我们需要的Python环境,比如我们的版本是3.8的Python,那我可能有一天换了一台电脑之后,我整套环境就需要全部重新搭建,不只是Python,我们一系列的第三方...
- 环境配置篇:Centos如何安装Python解释器
-
有小伙伴时常会使用Python进行编程,那么如何配置centos中的Python环境呢?1)先安装依赖yuminstallgccgcc-c++sqlite-devel在root用户下操作:1...
- (三)Centos7.6安装MySql(centos8.3安装docker)
-
借鉴文章:centos7+django+python3+mysql+阿里云部署项目全流程。这里我只借鉴安装MySql这一部分。链接:https://blog.csdn.net/a394268045/a...
- Centos7.9 如何安装最新版本的Docker
-
在CentOS7.9系统中安装最新版本的Docker,需遵循以下步骤,并注意依赖项的兼容性问题:1.卸载旧版本Docker(如已安装)若系统中存在旧版Docker,需先卸载以避免冲突:sudoy...
- Linux 磁盘空间不够用?5 招快速清理文件,释放 10GB 空间不是梦!
-
刚收到服务器警告:磁盘空间不足90%!装软件提示Nospaceleftondevice!连日志都写不进去,系统卡到崩溃?别慌!今天教你5个超实用的磁盘清理大招,从临时文件到无用软件一键搞定...
- Playwright软件测试框架学习笔记(playwright 官网)
-
本文为霍格沃兹测试开发学社学员学习笔记,人工智能测试开发进阶学习文末加群。一,Playwright简介Web自动化测试框架。跨平台多语言支持。支持Chromium、Firefox、WebKit...
- 为SpringDataJpa集成QueryObject模式
-
1.概览单表查询在业务开发中占比最大,是所有CRUDBoy的入门必备,所有人在JavaBean和SQL之间乐此不疲。而在我看来,该部分是最枯燥、最没有技术含量的“伪技能”。1.1.背景...
- 金字塔测试原理:写好单元测试的8个小技巧,一文总结
-
想必金字塔测试原理大家已经很熟悉了,近年来的测试驱动开放在各个公司开始盛行,测试代码先写的倡议被反复提及。鉴于此,许多中大型软件公司对单元测试的要求也逐渐提高。那么,编写单元测试有哪些小技巧可以借鉴和...
- 测试工程师通常用哪个单元测试库来测试Java程序?
-
测试工程师在测试Java程序时通常使用各种不同的单元测试库,具体选择取决于项目的需求和团队的偏好。我们先来看一些常用的Java单元测试库,以及它们的一些特点: 1.JUnit: ·描述:JUn...
- JAVA程序员自救之路——SpringAI评估
-
背景我们用SpringAI做了大模型的调用,RAG的实现。但是我们做的东西是否能满足我们业务的要求呢。比如我们问了一个复杂的问题,大模型能否快速准确的回答出来?是否会出现幻觉?这就需要我们构建一个完善...
- 一周热门
- 最近发表
-
- zabbix企业微信告警(zabbix5.0企业微信告警详细)
- 基于centos7部署saltstack服务器管理自动化运维平台
- 功能实用,效率提升,Python开发的自动化运维工具
- centos定时任务之python脚本(centos7执行python脚本)
- Fedora 41 终于要和 Python 2.7 说再见了
- 软件测试|使用docker搞定 Python环境搭建
- 环境配置篇:Centos如何安装Python解释器
- (三)Centos7.6安装MySql(centos8.3安装docker)
- Centos7.9 如何安装最新版本的Docker
- Linux 磁盘空间不够用?5 招快速清理文件,释放 10GB 空间不是梦!
- 标签列表
-
- ps图案在哪里 (33)
- super().__init__ (33)
- python 获取日期 (34)
- 0xa (36)
- super().__init__()详解 (33)
- python安装包在哪里找 (33)
- linux查看python版本信息 (35)
- python怎么改成中文 (35)
- php文件怎么在浏览器运行 (33)
- eval在python中的意思 (33)
- python安装opencv库 (35)
- python div (34)
- sticky css (33)
- python中random.randint()函数 (34)
- python去掉字符串中的指定字符 (33)
- python入门经典100题 (34)
- anaconda安装路径 (34)
- yield和return的区别 (33)
- 1到10的阶乘之和是多少 (35)
- python安装sklearn库 (33)
- dom和bom区别 (33)
- js 替换指定位置的字符 (33)
- python判断元素是否存在 (33)
- sorted key (33)
- shutil.copy() (33)