大家好,又见面了。

到这里呢,已经是本SpringData JPA系列文档的第三篇了,先来回顾下前面两篇:

本篇内容将在上一篇已有的内容基础上,进一步的聊一下项目中使用JPA的一些高阶复杂场景的实践指导,覆盖了主要核心的JPA使用场景,可以让你在需求开发的时候对JPA的使用更加的游刃有余。

Repository

上一篇文档中,我们知道业务代码中直接调用Repository层中默认提供的方法或者是自己自定义的接口方法,便可以进行DB的相关操作。这里我们再对repository的整体实现情况进一步探索下。

repository全貌梳理

先看下Repository相关的类图:

整体类图虽然咋看上去很庞杂,但其实主线脉络还是比较清晰的。

  • 先看下蓝色的部分其实就是Repository的一整个接口定义链条,而橙色的则是我们自己自定义的一些Repository接口类,继承父层接口的所有已有能力。
  • 左侧的类图与接口,其实都是JPA提供的一些用于实现或者定制查询操作的一些辅助实现类,后面章节中会看到他们的身影。

对主体repository层级提供的主要方法进行简单的梳理,如下:

下面对各个repository接口进行简单的独立介绍。

JpaRepository与它的父类们

  • Repository位于Spring Data Common的lib里面,是Spring Data 里面做数据库操作的最底层的抽象接口、最顶级的父类,源码里面其实什么方法都没有,仅仅起到一个标识作用。
  • CrudRepository作为直接继承Repository的次顶层接口类,看名字也可以大致猜测出其主要作用就是封装提供基础CRUD操作。
  • PagingAndSortingRepository继承自CrudRepository,自然也就具备了CrudRepository提供的全部接口能力。此外,从其自身新提供的接口来看,增加了排序和分页查询列表的能力,非常符合其类名的含义。

JpaRepository与其前面的几个父类相比是个特殊的存在,其中补充添加了一组JPA规范的接口方法。前面的几个接口类都是Spring Data为了兼容NoSQL而进行的一些抽象封装(因为SpringData项目是一个庞大的家族,支持各种SQL与NoSQL的数据库,SpringData JPA是SpringData家族中面向SQL数据库的一个子分支项目),从JpaRepository开始是对关系型数据库进行抽象封装。

从类图可以看得出来它继承了PagingAndSortingRepository类,也就继承了其所有方法,并且实现类也是SimpleJpaRepository。从类图上还可以看出JpaRepository继承和拥有了QueryByExampleExecutor的相关方法。

通过源码和CrudRepository相比较,它支持Query By Example,批量删除,提高删除效率,手动刷新数据库的更改方法,并将默认实现的查询结果变成了List。

额外补充一句:

实际的项目编码中,大部分的场景中,我们自定义Repository都是继承JpaRepository来实现的。

自定义Repository

先看个自定义Repository的例子,如下:

看下对应类图结构,自定义Repository继承了JpaRepository,具备了其父系所有的操作接口,此外,额外扩展了业务层面自定义的一些接口方法:

自定义Repository的时候,继承JpaRepository需要传入两个泛型:

  • 此Repository需要操作的具体Entity对象(Entity与具体DB中表映射,所以指定Entity也等同于指定了此Repository所对应的目标操作Table),
  • 此Entity实体的主键数据类型(也就是第一个参数指定的Entity类中以@Id注解标识的字段的类型)

分页、排序,一招搞定

分页,排序使用Pageable对象进行传递,其中包含PageSort参数对象。

查询的时候,直接传递Pageable参数即可(注意下,如果是用原生SQL查询的方式,此法行不通,后文有详细说明)。


// 定义repository接口的时候,直接传入Pageable参数即可
List<UserEntity> findAllByDepartment(DepartmentEntity department, Pageable pageable);

还有一种特殊的分页场景。比如,DB表中有100w条记录,然后现在需要将这些数据全量的加载到ES中。如果逐条查询然后插入ES,显然效率太慢;如果一次性全部查询出来然后直接往ES写,服务端内存可能会爆掉。

这种场景,其实可以基于Slice结果对象进行实现。Slice的作用是,只知道是否有下一个Slice可用,不会执行count,所以当查询较大的结果集时,只知道数据是足够的就可以了,而且相关的业务场景也不用关心一共有多少页。


private <T extends EsDocument, F> void fullLoadToEs(IESLoadService<T, F> esLoadService) {
try {
final int batchHandleSize = 10000;
Pageable pageable = PageRequest.of(0, batchHandleSize);
do {
// 批量加载数据,返回Slice类型结果
Slice<F> entitySilce = esLoadService.slicePageQueryData(pageable); // 具体业务处理逻辑
List<T> esDocumentData = esLoadService.buildEsDocumentData(entitySilce);
esUtil.batchSaveOrUpdateAsync(esDocumentData); // 获取本次实际上加载到的具体数据量
int pageLoadedCount = entitySilce.getNumberOfElements();
if (!entitySilce.hasNext()) {
break;
} // 自动重置page分页参数,继续拉取下一批数据
pageable = entitySilce.nextPageable();
} while (true);
} catch (Exception e) {
log.error("error occurred when load data into es", e);
}
}

复杂搜索,其实不复杂

按照条件进行搜索查询,是项目中遇到的非常典型且常用的场景。但是条件搜索也分几种场景,下面分开说下。

简单固定场景

所谓简单固定,即查询条件就是固定的1个字段或者若干个字段,且查询字段数量不会变,比如根据部门查询具体人员列表这种。

这种情况,我们可以简单的直接在repository中,根据命名规范定义一个接口即可。


@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
// 根据一个固定字段查询
List<UserEntity> findAllByDepartment(DepartmentEntity department);
// 根据多个固定字段组合查询
UserEntity findFirstByWorkIdAndUserNameAndDepartment(String workId, String userName, DepartmentEntity department);
}

简单不固定场景

考虑一种场景,界面上需要做一个用户搜索的能力,要求支持根据用户名、工号、部门、性别、年龄、职务等等若干个字段中的1个或者多个的组合来查询符合条件的用户信息。

显然,上述通过直接在repository中按照命名规则定义接口的方式行不通了。这个时候,Example对象便排上用场了。

其实在前面整体介绍Repository的UML图中,就已经有了Example的身影了,虽然这个名字起的很敷衍,但其功能确是挺实在的。

看下具体用法:


public Page<UserEntity> queryUsers(Request request, UserEntity queryParams) {
// 查询条件构造出对应Entity对象,转为Example查询条件
Example<UserEntity> example = Example.of(queryParams);
// 构造分页参数
Pageable pageable = PageHelper.buildPageable(request); // 按照条件查询,并分页返回结果
return userRepository.findAll(example, pageable);
}

复杂场景

如果是一些自定义的复杂查询场景,可以通过定制SQL语句的方式来实现。


@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
@Query(
value = "select t.*,(select group_concat(a.assigner_name) from workflow_task a where a.state='R' and a.proc_inst_id=t.proc_inst_id) deal_person,"
+ " (select a.task_name from workflow_task a where a.state='R' and a.proc_inst_id=t.proc_inst_id limit 1) cur_step "
+ " from workflow_info t where t.state='R' and t.type in (?1) "
+ "and exists(select 1 from workflow_task b where b.assigner=?2 and b.state='R' and b.proc_inst_id=t.proc_inst_id) order by t.create_time desc",
countQuery = "select count(1) from workflow_info t where t.state='R' and t.type in (?1) "
+ "and exists(select 1 from workflow_task b where b.assigner=?2 and b.state='R' and b.proc_inst_id=t.proc_inst_id) ",
nativeQuery = true)
Page<FlowResource> queryResource(List<String> type, String workId, Pageable pageable);
}

此外,还可以基于JpaSpecificationExecutor提供的能力接口来实现。

自定义接口需要增加JpaSpecificationExecutor的继承,然后利用Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);接口来实现复杂查询能力。


// 增加对JpaSpecificationExecutor的继承
@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long>, JpaSpecificationExecutor<UserEntity> { }

public List<UserEntity> queryUsers(QueryParams queryParams) {
// 构造Specification查询条件
Specification<UserEntity> specification =
(root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
// 范围查询条件构造
predicates.add(cb.greaterThanOrEqualTo(root.get("age"), queryParams.getMinAge()));
predicates.add(cb.lessThanOrEqualTo(root.get("age"), queryParams.getMaxAge()));
// 精确匹配查询条件构造
predicates.add(cb.equal(root.get("department"), queryParams.getDepartment()));
// 关键字模糊匹配条件构造
if (Objects.nonNull(queryParams.getNameKeyword())) {
predicates.add(cb.like(root.get("userName"), "%" + queryParams.getNameKeyword() + "%"));
}
return query.where(predicates.toArray(new Predicate[0])).getRestriction();
};
// 执行复杂查询条件
return userRepository.findAll(specification);
}

自定义Listener,玩出花样

实际项目中,经常会有一种场景,就是需要监听某个数据的变更然后做一些额外的处理逻辑。一种逻辑,是写操作的时候顺便调用下相关业务的处理API,这样会造成业务间耦合加深;优化点的策略是搞个MQ队列,然后在这个写DB操作的同时发个消息到MQ里面,然后一堆的consumer会监听MQ并去做对应的处理逻辑,这样引入个消息队列代价也有点高。

这个时候,我们可以借助JPA的自定义EntityListener功能来完美解决。通过监听某个Entity表的变更情况,通知或者调用相关其他的业务代码处理,完美实现了与主体业务逻辑的解耦,也无需引入其他组件。

举个例子:现有一个论坛发帖系统,发帖Post和评论Comment属于两个相对独立又有点关系的数据,现在需要检测当评论变化的时候,需要更新下Post对应记录的评论数字段。下面演示下具体实现。

  • 首先,定制一个Listener类,并指定Callbacks注解

public class CommentCountAuditListener {
/**
* 当Comment表有新增数据的操作时,触发此方法的调用
*/
@PostPersist
public void postPersist(CommentEntity entity) {
// 执行Post表中评论数字段的更新
// do something here...
} /**
* 当Comment表有删除数据的操作时,触发此方法的调用
*/
@PostRemove
public void postRemove(CommentEntity entity) {
// 执行Post表中评论数字段的更新
// do something here...
} /**
* 当Comment表有更新数据的操作时,触发此方法的调用
*/
@PostUpdate
public void postUpdate(CommentEntity entity) {
// 执行Post表中评论数字段的更新
// do something here...
} }
  • 其次,在评论实体CommentEntity上,加上自定义Listener信息

@Entity
@Table("t_comment")
// 指定前面定制的Listener
@EntityListeners({CommentCountAuditListener.class})
public class CommentEntity extends AbstractAuditable {
// ...
}

这样就搞定了。

自定义Listener还有个典型的使用场景,就是可以统一的记录DB数据的操作日志。

定制化SQL,随心所欲

JPA提供@Query注解,可以实现自定义SQL语句的能力。比如:


@Query(value = "select * from user " +
"where work_id in (?1) " +
"and department_id = 0 " +
"order by CREATE_TIME desc ",
nativeQuery = true)
List<OssFileInfoEntity> queryUsersByWorkIdIn(List<String> workIds);

如果需要执行写操作SQL的时候,需要额外增加@Modifying注解标识,如下:


@Modifying
@Query(value = "insert into user (work_id, user_name) values (?1, ?2)",
nativeQuery = true)
int createUser(String workId, String userName);

其中,nativeQuery = true表示@Query注解中提供的value值为原生SQL语句。如果nativeQuery未设置或者设置为false,则表示将使用JPQL语言来执行。所谓JPQL,即JAVA持久化查询语句,是一种类似SQL的语法,不同点在于其使用类名来替代表名,使用类字段来替代表字段名。比如:


@Query("SELECT u FROM com.vzn.demo.UserInfo u WHERE u.userName = ?1")
public UserInfo getUserInfoByName(String name);

几个关注点要特别阐述下:

  • like查询的时候,参数前后的%需要手动添加,系统是不会自动加上的

// like 需要手动添加百分号
@Query("SELECT u FROM com.vzn.demo.UserInfo u WHERE u.userName like %?1")
public UserInfo getUserInfoByName(String name);
  • 使用nativeQuery=true查询的时候(原生SQL方式),不支持API接口里面传入Sort对象然后进行混合执行

// 错误示范: 自定义sql与API中Sort参数不可同时混用
@Query("SELECT * FROM t_user u WHERE u.user_name = ?1", nativeQuery=true)
public UserInfo getUserInfoByName(String name, Sort sort); // 正确示范: 自定义SQL完成对应sort操作
@Query("SELECT * FROM t_user u WHERE u.user_name = ?1 order by ?2", nativeQuery=true)
public UserInfo getUserInfoByName(String name, String sortColumn);
  • 未指定nativeQuery=true查询的时候(JPQL方式),支持API接口里面传入SortPageRequest等对象然后进行混合执行,来完成排序、分页等操作

// 正确:自定义jpql与API中Sort参数不可同时混用
@Query("SELECT u FROM com.vzn.demo.UserInfo u WHERE u.userName = ?1")
public UserInfo getUserInfoByName(String name, Sort sort);
  • 支持使用参数名作为@Query查询中的SQL或者JPQL语句的入参,取代参数顺序占位符

默认情况下,参数是通过顺序绑定在自定义执行语句上的,这样如果API接口传参顺序或者位置改变,极易引起自定义查询传参出问题,为了解决此问题,我们可以使用@Param注解来绑定一个具体的参数名称,然后以参数名称的形式替代位置顺序占位符,这也是比较推荐的一种做法。


// 默认的顺序位置传参
@Query("SELECT * FROM t_user u WHERE u.user_name = ?1 order by ?2", nativeQuery=true)
public UserInfo getUserInfoByName(String name, String sortColumn); // 使用参数名称传参
@Query("SELECT * FROM t_user u WHERE u.user_name = :name order by :sortColumn", nativeQuery=true)
public UserInfo getUserInfoByName(@Param("name") String name, @Param("sortColumn") String sortColumn);

字段命名映射策略

一般而言,JAVA的编码规范都要求filed字段命名需要遵循小驼峰命名的规范,比如userName,而DB中column命名的时候,很多人习惯于使用下划线分隔的方式命名,比如user_name这种。这样就涉及到一个映射的策略问题,需要让JPA知道代码里面的userName就对应着DB中的user_name

这里就会涉及到对命名映射策略的映射。主要有两种映射配置,下面分别阐述下。

  • implicit-strategy

配置项key值:

spring.jpa.hibernate.naming.implicit-strategy=xxxxx

取值说明:

映射规则说明
org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImp 默认的命名策略,兼容JPA2.0规范
org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyHbmImpl 兼容老版本Hibernate的命名规范
org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl 与ImplicitNamingStrategyJpaCompliantImp基本相同
org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl 兼容JPA 1.0规范中的命名规范。
org.hibernate.boot.model.naming.SpringImplicitNamingStrategy 继承ImplicitNamingStrategyJpaCompliantImpl,对外键、链表查询、索引如果未定义,都有下划线的处理策略,而table和column名字都默认与字段一样
  • physical-strategy

配置项key值:

spring.jpa.hibernate.naming.physical-strategy=xxxxx

取值说明:

映射规则说明
org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl 默认字符串一致映射,不做任何转换处理,比如java类中userName,映射到table中列名也叫userName
org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy java类中filed名称小写字母进行映射到DB表column名称,遇大写字母时转为分隔符"_"命名格式,比如java类中userName字段,映射到DB表column名称叫user_name
  • physical-strategy与implicit-strategy

SpringData JPA只是对JPA规范的二次封装,其底层使用的是Hibernate,所以此处涉及到Hibernate提供的一些处理策略。Hibernate将对象模型映射到关系数据库分为两个步骤:

  1. 从对象模型中确定逻辑名称。逻辑名可以由用户显式指定(使用@Column@Table),也可以隐式指定。
  2. 将逻辑名称映射到物理名称,也就是数据库中使用的名称。

这里,implicit-strategy用于第一步隐式指定逻辑名称,而physical-strategy则用于第二步中逻辑名称到物理名称的映射。

注意:

当没有使用@Table@Column注解时,implicit-strategy配置项才会被使用,即implicit-strategy定义的是一种缺省场景的处理策略;而physical-strategy属于一种高优先级的策略,只要设置就会被执行,而不管是否有@Table@Column注解。

小结,承上启下

好啦,本篇内容就介绍到这里。

通过本篇的内容,我们对于如何在项目中使用Spring Data JPA来进行一些较为复杂场景的处理方案与策略有了进一步的了解,再结合本系列此前的内容,到此掌握的JPA的相关技能已经足以应付大部分项目开发场景。

在实际项目中,为了保障数据操作的可靠、避免脏数据的产生,需要在代码中加入对数据库操作的事务控制。在下一篇文档中,我们将一起聊一聊Spring Data JPA业务代码开发中关于数据库事务的控制,以及编码中存在哪些可能会导致事务失效的场景等等。

如果对本文有自己的见解,或者有任何的疑问或建议,都可以留言,我们一起探讨、共同进步。


补充

Spring Data JPA作为Spring Data中对于关系型数据库支持的一种框架技术,属于ORM的一种,通过得当的使用,可以大大简化开发过程中对于数据操作的复杂度。

本文档隶属于《Spring Data JPA用法与技能探究》系列的第3篇。本系列文档规划对Spring Data JPA进行全方位的使用介绍,一共分为5篇文档,如果感兴趣,欢迎关注交流。

《Spring Data JPA用法与技能探究》系列涵盖内容:


我是悟道,聊技术、又不仅仅聊技术~

如果觉得有用,请点个关注,也可以关注下我的公众号【架构悟道】,获取更及时的更新。

期待与你一起探讨,一起成长为更好的自己。

最新文章

  1. React学习笔记-6-运行中阶段介绍
  2. SerialPort基本小例
  3. spring beans源码解读之--XmlBeanFactory
  4. C++ Primer的课后规划问题的第八章
  5. 关于getchar()的知识
  6. hdu-5786(补图最短路)
  7. Android--UI之TextView
  8. shell脚本学习1(Linux脚本攻略)
  9. python dict的函数
  10. win php安装 oracle11 g
  11. 170814、Java使用gzip压缩文件、还原文件
  12. linux服务器上nginx日志访问量统计命令
  13. 自己写的一个delphi正整数快速排序
  14. Centos7 創建快捷方式
  15. webpack的基础入门
  16. Linux configure关于交叉编译的参数设置【转】
  17. 换个思路理解Javascript中的this
  18. vue学习(一)、Vue.js简介
  19. 检查SQL Server 2005的索引密度和碎片信息(转)
  20. 网站定时任务IIS配置

热门文章

  1. Vue.js Mixins 混入使用
  2. 通过源码了解Java的自动装箱拆箱
  3. 关于python中selenium一些知识点
  4. Java基础语法Day_05(数组的概念)
  5. C#中检查null的语法糖
  6. 携程开源分布式配置系统Apollo服务端是如何实时更新配置的?
  7. 忘记VMware vcenter的Administrator@vsphere.local密码
  8. Hyperledger Fabric 2.2 学习笔记:测试网络test-network
  9. Spring Ioc源码分析系列--前言
  10. 开源框架 WebFirst 一键生成项目,在线建表