为SpringDataJpa集成QueryObject模式

310 篇文章 6 订阅
298 篇文章 3 订阅

1.概览

单表查询在业务开发中占比最大,是所有 CRUD Boy 的入门必备,所有人在 JavaBean 和 SQL 之间乐此不疲。而在我看来,该部分是最枯燥、最没有技术含量的“伪技能”。

1.1. 背景

针对单表查询的 JPA 封装,很多读者反馈很方便也很简单,确实能解决了不少问题:

  1. 不需要写 SQL,能够快速实现,提升开发效率
  2. 避免查询条件不当引起的性能问题

但,有眼光锐利的读者提出一个问题:为什么要在 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 继承两个接口:

  1. Repository,标记为一个仓库,由 spring data 为其创建代理类;
  2. 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> {
}

其中:

  1. JpaRepository 为 spring data jpa 提供的 Repository 扩展;
  2. 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 集成后,使用变得非常简单:

  1. 符合 Spring data 的使用风格,降低使用门槛;
  2. 无需定义新的 Repository 体系,避免逻辑分离;
  3. 最关键的是,所提供的能力没有任何减少;

4. 设计&扩展

4.1. 功能扩展

spring data jpa 具有非常强的扩展性,整体如下:

 

image

具体扩展点包括:

  1. QueryObjectRepository 定义一套基于 “查询对象” 的查询接口,包括 get、listOf、pageOf、countOf;
  2. SpecificationQueryObjectRepository 扩展自 QueryObjectRepository,基于 Specification 实现查询;
  3. JpaBasedQueryObjectRepository 实现 SpecificationQueryObjectRepository 接口,扩展自 SimpleJpaRepository,在 SimpleJpaRepository 基础上提供 SpecificationQueryObjectRepository 的通用实现;

4.2. 框架集成

 

image

与框架集成的核心在
JpaBasedQueryObjectRepositoryFactoryBean,也就是在 @EnableJpaRepositories 的 repositoryFactoryBeanClass 指定的类,该类主要用于为每个 Repository 提供 RepositoryFactory,有 RepositoryFactory 为其生成代理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值