大家好,又见面了。
到这里呢,已经是本SpringDataJPA系列文档的第三篇了,先来回顾下前面两篇:
在第1篇《SpringDataJPA系列1:JDBC、ORM、JPA、SpringDataJPA,傻傻分不清楚?给你个选择SpringDataJPA的理由!》中,我们对JPA的整体概念有了全面的了解。
在第2篇《SpringDataJPA系列2:快速在SpringBoot项目中熟练使用JPA》中也知晓了SpringBoot项目快速集成SpringDataJPA以及快速上手使用JPA来进行基本的项目开发的技能。
本篇内容将在已有的内容基础上,进一步的聊一下项目中使用JPA的一些高阶复杂场景的实践指导,覆盖了主要核心的JPA使用场景,可以让你在需求开发的时候对JPA的使用更加的游刃有余。
Repository
文档中,我们知道业务代码中直接调用Repository层中默认提供的方法或者是自己自定义的接口方法,便可以进行DB的相关操作。这里我们再对repository的整体实现情况进一步探索下。
repository全貌梳理
先看下Repository相关的类图:
整体类图虽然咋看上去很庞杂,但其实主线脉络还是比较清晰的。
先看下蓝色的部分其实就是Repository的一整个接口定义链条,而橙色的则是我们自己自定义的一些Repository接口类,继承父层接口的所有已有能力。
左侧的类图与接口,其实都是JPA提供的一些用于实现或者定制查询操作的一些辅助实现类,后面章节中会看到他们的身影。
对主体repository层级提供的主要方法进行简单的梳理,如下:
下面对各个repository接口进行简单的独立介绍。
JpaRepository与它的父类们
Repository位于SpringDataCommon的lib里面,是SpringData里面做数据库操作的最底层的抽象接口、最顶级的父类,源码里面其实什么方法都没有,仅仅起到一个标识作用。
CrudRepository作为直接继承Repository的次顶层接口类,看名字也可以大致猜测出其主要作用就是封装提供基础CRUD操作。
PagingAndSortingRepository继承自CrudRepository,自然也就具备了CrudRepository提供的全部接口能力。此外,从其自身新提供的接口来看,增加了排序和分页查询列表的能力,非常符合其类名的含义。
JpaRepository与其前面的几个父类相比是个特殊的存在,其中补充添加了一组JPA规范的接口方法。前面的几个接口类都是SpringData为了兼容NoSQL而进行的一些抽象封装(因为SpringData项目是一个庞大的家族,支持各种SQL与NoSQL的数据库,SpringDataJPA是SpringData家族中面向SQL数据库的一个子分支项目),从JpaRepository开始是对关系型数据库进行抽象封装。
从类图可以看得出来它继承了PagingAndSortingRepository类,也就继承了其所有方法,并且实现类也是SimpleJpaRepository。从类图上还可以看出JpaRepository继承和拥有了QueryByExampleExecutor的相关方法。
通过源码和CrudRepository相比较,它支持QueryByExample,批量删除,提高删除效率,手动刷新数据库的更改方法,并将默认实现的查询结果变成了List。
额外补充一句:
实际的项目编码中,大部分的场景中,我们自定义Repository都是继承JpaRepository来实现的。
自定义Repository
先看个自定义Repository的例子,如下:
看下对应类图结构,自定义Repository继承了JpaRepository,具备了其父系所有的操作接口,此外,额外扩展了业务层面自定义的一些接口方法:
自定义Repository的时候,继承JpaRepository需要传入两个泛型:
此Repository需要操作的具体Entity对象(Entity与具体DB中表映射,所以指定Entity也等同于指定了此Repository所对应的目标操作Table),
此Entity实体的主键数据类型(也就是第一个参数指定的Entity类中以
Id注解标识的字段的类型)分页、排序,一招搞定
分页,排序使用Pageable对象进行传递,其中包含Page和Sort参数对象。
查询的时候,直接传递Pageable参数即可(注意下,如果是用原生SQL查询的方式,此法行不通,后文有详细说明)。
//定义repository接口的时候,直接传入Pageable参数即可ListUserEntityfindAllByDepartment(DepartmentEntitydepartment,Pageablepageable);
还有一种特殊的分页场景。比如,DB表中有w条记录,然后现在需要将这些数据全量的加载到ES中。如果逐条查询然后插入ES,显然效率太慢;如果一次性全部查询出来然后直接往ES写,服务端内存可能会爆掉。
这种场景,其实可以基于Slice结果对象进行实现。Slice的作用是,只知道是否有下一个Slice可用,不会执行count,所以当查询较大的结果集时,只知道数据是足够的就可以了,而且相关的业务场景也不用关心一共有多少页。
privateTextendsEsDocument,FvoidfullLoadToEs(IESLoadServiceT,FesLoadService){try{finalintbatchHandleSize=00;Pageablepageable=PageRequest.of(0,batchHandleSize);do{//批量加载数据,返回Slice类型结果SliceFentitySilce=esLoadService.slicePageQueryData(pageable);//具体业务处理逻辑ListTesDocumentData=esLoadService.buildEsDocumentData(entitySilce);esUtil.batchSaveOrUpdateAsync(esDocumentData);//获取本次实际上加载到的具体数据量intpageLoadedCount=entitySilce.getNumberOfElements();if(!entitySilce.hasNext()){break;}//自动重置page分页参数,继续拉取下一批数据pageable=entitySilce.nextPageable();}while(true);}catch(Exceptione){log.error("erroroccurredwhenloaddataintoes",e);}}
复杂搜索,其实不复杂
按照条件进行搜索查询,是项目中遇到的非常典型且常用的场景。但是条件搜索也分几种场景,下面分开说下。
简单固定场景
所谓简单固定,即查询条件就是固定的1个字段或者若干个字段,且查询字段数量不会变,比如根据部门查询具体人员列表这种。这种情况,我们可以简单的直接在repository中,根据命名规范定义一个接口即可。
RepositorypublicinterfaceUserRepositoryextendsJpaRepositoryUserEntity,Long{//根据一个固定字段查询ListUserEntityfindAllByDepartment(DepartmentEntitydepartment);//根据多个固定字段组合查询UserEntityfindFirstByWorkIdAndUserNameAndDepartment(StringworkId,StringuserName,DepartmentEntitydepartment);}简单不固定场景
考虑一种场景,界面上需要做一个用户搜索的能力,要求支持根据用户名、工号、部门、性别、年龄、职务等等若干个字段中的1个或者多个的组合来查询符合条件的用户信息。显然,上述通过直接在repository中按照命名规则定义接口的方式行不通了。这个时候,Example对象便排上用场了。
其实在前面整体介绍Repository的UML图中,就已经有了Example的身影了,虽然这个名字起的很敷衍,但其功能确是挺实在的。
看下具体用法:
publicPageUserEntityqueryUsers(Requestrequest,UserEntityqueryParams){//查询条件构造出对应Entity对象,转为Example查询条件ExampleUserEntityexample=Example.of(queryParams);//构造分页参数Pageablepageable=PageHelper.buildPageable(request);//按照条件查询,并分页返回结果returnuserRepository.findAll(example,pageable);}
复杂场景
如果是一些自定义的复杂查询场景,可以通过定制SQL语句的方式来实现。
RepositorypublicinterfaceUserRepositoryextendsJpaRepositoryUserEntity,Long{Query(value="selectt.*,(selectgroup_concat(a.assigner_name)fromworkflow_taskawherea.state=Randa.proc_inst_id=t.proc_inst_id)deal_person,"+"(selecta.task_namefromworkflow_taskawherea.state=Randa.proc_inst_id=t.proc_inst_idlimit1)cur_step"+"fromworkflow_infotwheret.state=Randt.typein(?1)"+"andexists(select1fromworkflow_taskbwhereb.assigner=?2andb.state=Randb.proc_inst_id=t.proc_inst_id)orderbyt.create_timedesc",countQuery="selectcount(1)fromworkflow_infotwheret.state=Randt.typein(?1)"+"andexists(select1fromworkflow_taskbwhereb.assigner=?2andb.state=Randb.proc_inst_id=t.proc_inst_id)",nativeQuery=true)PageFlowResourcequeryResource(ListStringtype,StringworkId,Pageablepageable);}此外,还可以基于JpaSpecificationExecutor提供的能力接口来实现。自定义接口需要增加JpaSpecificationExecutor的继承,然后利用PageTfindAll(
NullableSpecificationTspec,Pageablepageable);接口来实现复杂查询能力。//增加对JpaSpecificationExecutor的继承
RepositorypublicinterfaceUserRepositoryextendsJpaRepositoryUserEntity,Long,JpaSpecificationExecutorUserEntity{}publicListUserEntityqueryUsers(QueryParamsqueryParams){//构造Specification查询条件SpecificationUserEntityspecification=(root,query,cb)-{ListPredicatepredicates=newArrayList();//范围查询条件构造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()+"%"));}returnquery.where(predicates.toArray(newPredicate[0])).getRestriction();};//执行复杂查询条件returnuserRepository.findAll(specification);}
自定义Listener,玩出花样
实际项目中,经常会有一种场景,就是需要监听某个数据的变更然后做一些额外的处理逻辑。一种逻辑,是写操作的时候顺便调用下相关业务的处理API,这样会造成业务间耦合加深;优化点的策略是搞个MQ队列,然后在这个写DB操作的同时发个消息到MQ里面,然后一堆的consumer会监听MQ并去做对应的处理逻辑,这样引入个消息队列代价也有点高。
这个时候,我们可以借助JPA的自定义EntityListener功能来完美解决。通过监听某个Entity表的变更情况,通知或者调用相关其他的业务代码处理,完美实现了与主体业务逻辑的解耦,也无需引入其他组件。
举个例子:现有一个论坛发帖系统,发帖Post和评论Comment属于两个相对独立又有点关系的数据,现在需要检测当评论变化的时候,需要更新下Post对应记录的评论数字段。下面演示下具体实现。
首先,定制一个Listener类,并指定Callbacks注解
publicclassCommentCountAuditListener{/***当Comment表有新增数据的操作时,触发此方法的调用*/
PostPersistpublicvoidpostPersist(CommentEntityentity){//执行Post表中评论数字段的更新//dosomethinghere...}/***当Comment表有删除数据的操作时,触发此方法的调用*/PostRemovepublicvoidpostRemove(CommentEntityentity){//执行Post表中评论数字段的更新//dosomethinghere...}/***当Comment表有更新数据的操作时,触发此方法的调用*/PostUpdatepublicvoidpostUpdate(CommentEntityentity){//执行Post表中评论数字段的更新//dosomethinghere...}}其次,在评论实体CommentEntity上,加上自定义Listener信息
EntityTable("t_