大家好,又见面了。

到这里呢,已经是本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查询的方式,此法行不通,后文有详细说明)。


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

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

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


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

复杂搜索,其实不复杂

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

简单固定场景

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

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


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

简单不固定场景

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

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

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

看下具体用法:


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

复杂场景

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


  1. @Repository
  2. public interface UserRepository extends JpaRepository<UserEntity, Long> {
  3. @Query(
  4. 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,"
  5. + " (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 "
  6. + " from workflow_info t where t.state='R' and t.type in (?1) "
  7. + "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",
  8. countQuery = "select count(1) from workflow_info t where t.state='R' and t.type in (?1) "
  9. + "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) ",
  10. nativeQuery = true)
  11. Page<FlowResource> queryResource(List<String> type, String workId, Pageable pageable);
  12. }

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

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


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

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

自定义Listener,玩出花样

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

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

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

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

  1. public class CommentCountAuditListener {
  2. /**
  3. * 当Comment表有新增数据的操作时,触发此方法的调用
  4. */
  5. @PostPersist
  6. public void postPersist(CommentEntity entity) {
  7. // 执行Post表中评论数字段的更新
  8. // do something here...
  9. }
  10. /**
  11. * 当Comment表有删除数据的操作时,触发此方法的调用
  12. */
  13. @PostRemove
  14. public void postRemove(CommentEntity entity) {
  15. // 执行Post表中评论数字段的更新
  16. // do something here...
  17. }
  18. /**
  19. * 当Comment表有更新数据的操作时,触发此方法的调用
  20. */
  21. @PostUpdate
  22. public void postUpdate(CommentEntity entity) {
  23. // 执行Post表中评论数字段的更新
  24. // do something here...
  25. }
  26. }
  • 其次,在评论实体CommentEntity上,加上自定义Listener信息

  1. @Entity
  2. @Table("t_comment")
  3. // 指定前面定制的Listener
  4. @EntityListeners({CommentCountAuditListener.class})
  5. public class CommentEntity extends AbstractAuditable {
  6. // ...
  7. }

这样就搞定了。

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

定制化SQL,随心所欲

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


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

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


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

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


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

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

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

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

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

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

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


  1. // 默认的顺序位置传参
  2. @Query("SELECT * FROM t_user u WHERE u.user_name = ?1 order by ?2", nativeQuery=true)
  3. public UserInfo getUserInfoByName(String name, String sortColumn);
  4. // 使用参数名称传参
  5. @Query("SELECT * FROM t_user u WHERE u.user_name = :name order by :sortColumn", nativeQuery=true)
  6. 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值:

  1. 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值:

  1. 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用法与技能探究》系列涵盖内容:


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

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

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

Spring Data JPA系列3:JPA项目中核心场景与进阶用法介绍的更多相关文章

  1. Java Web学习系列——Maven Web项目中集成使用Spring、MyBatis实现对MySQL的数据访问

    本篇内容还是建立在上一篇Java Web学习系列——Maven Web项目中集成使用Spring基础之上,对之前的Maven Web项目进行升级改造,实现对MySQL的数据访问. 添加依赖Jar包 这 ...

  2. java项目中ehcache缓存最简单用法

      java项目中ehcache缓存最简单用法: 1.下载ehcache-core-2.4.3.jar复制到项目的lib目录下 2.新建ehcache.xml文件,放置在项目src目录下的resour ...

  3. SpringBoot系列: SpringBoot Web项目中使用Shiro 之二

    ==================================Shiro 的加深理解:==================================1. Shiro 和 Spring 系组 ...

  4. Java Web学习系列——Maven Web项目中集成使用Spring

    参考Java Web学习系列——创建基于Maven的Web项目一文,创建一个名为LockMIS的Maven Web项目. 添加依赖Jar包 推荐在http://mvnrepository.com/.h ...

  5. SpringBoot系列: SpringBoot Web项目中使用Shiro

    注意点有:1. 不要启用 spring-boot-devtools, 如果启用 devtools 后, 不管是热启动还是手工重启, devtools总是试图重新恢复之前的session数据, 很有可能 ...

  6. Vue系列——在vue项目中使用echarts

    该示例使用 vue-cli 脚手架搭建 安装echarts依赖 npm install echarts -S 或者使用国内的淘宝镜像安装 npm install -g cnpm --registry= ...

  7. maven新建Spring MVC + MyBatis + Oracle的Web项目中pom.xml文件

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/20 ...

  8. Angular项目中核心模块core Module只加载一次的实现

    核心模块CoreModule在整个系统中只加载一次,如何实现? 创建core Modele:ng g m core 既然CoreModule是类,就有构造函数,在构造函数中进行依赖注入. export ...

  9. [AS3]as3中splice和slice的用法介绍说明

    splice 删除数组一段连续的元素,返回被删除的元素数组 var arr:Array = ["a","b","c","d&quo ...

随机推荐

  1. Transactional事务,事务嵌套的时候,如果主事务出现问题,子事务执行不需要回滚怎么做?

    如果调用的方法在不在同一个service当中,则只需要在子事务当中的方法上方添加注解即可 下方即是:这就话代表:重新开启一个新的事务 @Transactional(propagation = Prop ...

  2. Electron结合React和TypeScript进行开发

    目录 结合React+TypeScript进行Electron开发 1. electron基本简介 为什么选择electron? 2. 快速上手 2.1 安装React(template为ts) 2. ...

  3. 阿里云服务器的购买、基本配置、(xshell)远程连接、搭建环境

    一.服务器的购买 1.购买时间点:搞活动的时候.利用学生身份购买 (1)活动:想白嫖一台服务器 双十一,可以在双十一左右,时间提前一点,百度或B站,搜阿里云服务器.腾讯服务器(618可能也有) 一般, ...

  4. 修复Arch Linux和Manjaro Linux无法显示emoji的问题

    安装好Arch Linux或Manjaro Linux系统后默认没办法正常显示emoji,通常会变成方框或者带有unicode码的方块: 这是因为缺失字体以及相关的字体配置导致的. 当然也有一小部分应 ...

  5. 3.yum学习笔记

    一.yum介绍 将所有的rpm软件包放到指定服务器上,当进行yum在线安装时,可以自动解决依赖性问题. yum配置文件常位于/etc/yum.repo.d 目录下 [root@aaa251 ~]# c ...

  6. drf-Serializers

    What is serializers? serializers主要作用是将原生的Python数据类型(如 model querysets )转换为web中通用的JSON,XML或其他内容类型. DR ...

  7. leetcode704二分查找

    title: 二分查找 题目描述 题目链接:二分查找 解题思路 二分模板默写 int search(vector<int>& nums, int target) { int lef ...

  8. Nginx的mirror指令能干啥?

    mirror 流量复制 Nginx的 mirror 指令来自于 ngx_http_mirror_module 模块 Nginx Version > 1.13.4 mirror 指令提供的核心功能 ...

  9. CSS 不规则的轮廓-outline

    大家好,我是半夏,一个刚刚开始写文的沙雕程序员.如果喜欢我的文章,可以关注 点赞 加我微信:frontendpicker,一起学习交流前端,成为更优秀的工程师-关注公众号:搞前端的半夏,了解更多前端知 ...

  10. 被迫开始学习Typescript —— interface

    一开始以为,需要使用 class 来定义呢,学习之后才发现,一般都是使用 interface 来定义的. 这个嘛,倒是挺适合 js 环境的. 参考:https://typescript.bootcss ...