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 具有非常强的扩展性,整体如下:

具体扩展点包括:
-
QueryObjectRepository 定义一套基于 “查询对象” 的查询接口,包括 get、listOf、pageOf、countOf;
-
SpecificationQueryObjectRepository 扩展自 QueryObjectRepository,基于 Specification 实现查询;
-
JpaBasedQueryObjectRepository 实现 SpecificationQueryObjectRepository 接口,扩展自 SimpleJpaRepository,在 SimpleJpaRepository 基础上提供 SpecificationQueryObjectRepository 的通用实现;
4.2. 框架集成

与框架集成的核心在 JpaBasedQueryObjectRepositoryFactoryBean,也就是在 @EnableJpaRepositories 的 repositoryFactoryBeanClass 指定的类,该类主要用于为每个 Repository 提供 RepositoryFactory,有 RepositoryFactory 为其生成代理。
5. 项目信息
项目仓库地址:https://gitee.com/litao851025/lego
项目文档地址:https://gitee.com/litao851025/lego/wikis/support/SingleQuery-spring-data-jpa
原文始发于微信公众号(geekhalo):为SpringDataJpa集成QueryObject模式
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/60463.html