百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Spring Data Jpa最佳实践(spring data jpa 教程)

ccwgpt 2024-09-21 13:44 24 浏览 0 评论

前言

Spring Data Jpa框架的目标是显著减少实现各种持久性存储的数据访问层所需的样板代码量。Spring Data Jpa存储库抽象中的中央接口是Repository。它需要领域实体类以及领域实体ID类型作为类型参数来进行管理。该接口主要用作标记接口,以捕获要使用的类型并帮助您发现扩展该接口的接口。CrudRepository、JpaRepository是更具体的数据操作抽象,一般我们在项目中使用的时候定义我们的领域接口然后继承CrudRepository或JpaRepository即可实现实现基础的CURD方法了,但是这种用法有局限性,不能处理超复杂的查询,而且稍微复杂的查询代码写起来也不是很优雅,所以下面看看怎么最优雅的解决这个问题。

扩展接口用法

/**
 * @author: kl @kailing.pub
 * @date: 2019/11/11
 */
@Repository
public interface SendLogRepository extends JpaRepository {

 /**
 * 派生的通过解析方法名称的查询
 * @param templateName
 * @return
 */
 ListfindSendLogByTemplateName(String templateName);

 /**
 * HQL
 * @param templateName
 * @return
 */
 @Query(value ="select SendLog from SendLog s where s.templateName = :templateName")
 ListfindByTempLateName(String templateName);

 /**
 * 原生sql
 * @param templateName
 * @return
 */
 @Query(value ="select s.* from sms_sendlog s where s.templateName = :templateName",nativeQuery = true)
 ListfindByTempLateNameNative(String templateName);
}

优点:

  • 1、这种扩展接口的方式是最常见的用法,继承JpaRepository接口后,立马拥有基础的CURD功能
  • 2、还可以通过特定的方法名做解析查询,这个可以算spring Data Jpa的最特殊的特性了。而且主流的IDE对这种使用方式都有比较好的自动化支持,在输入要解析的方法名时会给出提示。
  • 3、可以非常方便的以注解的形式支持HQL和原生SQL

缺陷:

  • 1、复杂的分页查询支持不好

缺陷就一条,这种扩展接口的方式要实现复杂的分页查询,有两种方式,而且这两种方式代码写起来都不怎么优雅,而且会把大量的条件拼接逻辑写在调用查询的service层。

  • 第一种、实例查询(Example Query)方式:
 public void testExampleQuery() {
 SendLog log = new SendLog();
 log.setTemplateName("kl");
 /*
 * 注意:withMatcher方法的propertyPath参数值填写领域对象的字段值,而不是实际的表字段
 */
 ExampleMatcher matcher = ExampleMatcher.matching()
 .withMatcher("templateName", match -> match.contains());
 Example example = Example.of(log, matcher);

 Pageable pageable = PageRequest.of(0, 10);
 Page logPage = repository.findAll(example, pageable);
 }

上面代码实现的语义是模糊查询templateName等于"kl"的记录并分页,乍一看这个代码还过得去哈,其实当查询的条件多一点,这种代码就会变得又臭又长,而且只支持基础的字符串类型的字段查询,如果查询条件有时间筛选的话就不支持了,在复杂点多表关联的话就更GG了,所以这种方式不合格直接上黑名单了。

  • 第二种、继承JpaSpecificationExecutor方式:

JPA 2引入了一个标准API,您可以使用它来以编程方式构建查询。Spring Data JPA提供了使用JPA标准API定义此类规范的API。这种方式首先需要继承JpaSpecificationExecutor接口,下面我们用这种方式实现和上面相同语义的查询:

 public void testJpaSpecificationQuery() {
 String templateName = "kk";
 Specification specification = (Specification) (root, query, criteriaBuilder) -> {
 Predicate predicate = criteriaBuilder.like(root.get("templateName"),templateName);
 query.where(predicate);
 return predicate;
 };

 Pageable pageable = PageRequest.of(0, 2);
 Page logPage = sendLogRepository.findAll(specification, pageable);
 }

这种方式显然更对味口了吧,而且也支持复杂的查询条件拼接,比如日期等。唯一的缺憾是领域对象的属性字符串需要手写了,而且接口只会提供findAll(@Nullable Specificationspec, Pageable pageable)方法,各种复杂查询逻辑拼接都要写在service层。对于架构分层思想流行了这么多年外加强迫症的人来说实在是不能忍,如果单独封装一个Dao类编写复杂的查询又显的有点多余和臃肿

SPRING DATA JPA最佳实践

在详细介绍最佳实践前,先思考和了解一个东西,Spring Data Jpa是怎么做到继承一个接口就能实现各种复杂查询的呢?这里其实是一个典型的代理模式的应用,只要继承了最底层的Repository接口,在应用启动时就会帮你生成一个代理实例,而真正的目标类才是最终执行查询的类,这个类就是:SimpleJpaRepository,它实现了JpaRepository、JpaSpecificationExecutor的所有接口,所以只要基于SimpleJpaRepository定制Repository基类,就能拥有继承接口一样的查询功能,而且可以在实现类里编写复杂的查询方法了。

一、继承SIMPLEJPAREPOSITORY实现类

/**
 * @author: kl @kailing.pub
 * @date: 2019/11/8
 */
public abstract class BaseJpaRepository extends SimpleJpaRepository {

 public EntityManager em;

 BaseJpaRepository(ClassdomainClass, EntityManager em) {
 super(domainClass, em);
 this.em = em;
 }
}

构造一个SimpleJpaRepository实例,只需要一个领域对象的类型,和EntityManager 实例即可,EntityManager在Spring的上下文中已经有了,会自动注入。领域对象类型在具体的实现类中注入即可。如:

/**
 * @author: kl @kailing.pub
 * @date: 2019/11/11
 */
@Repository
public class SendLogJpaRepository extends BaseJpaRepository {
 
 public SendLogJpaRepository(EntityManager em) {
 super(SendLog.class, em);
 }
 /**
 * 原生查询
 * @param templateName
 * @return
 */
 public SendLog findByTemplateName(String templateName){
 String sql = "select * from send_log where templateName = :templateName";
 Query query =em.createNativeQuery(sql);
 query.setParameter("templateName",templateName);
 return (SendLog) query.getSingleResult();
 }

 /**
 * hql查询
 * @param templateName
 * @return
 */
 public SendLog findByTemplateNameNative(String templateName){
 String hql = "from SendLog where templateName = :templateName";
 TypedQueryquery =em.createQuery(hql,SendLog.class);
 query.setParameter("templateName",templateName);
 return query.getSingleResult();
 }

 /**
 * JPASpecification 实现复杂分页查询
 * @param logDto
 * @param pageable
 * @return
 */
 public PagefindAll(SendLogDto logDto,Pageable pageable) {
 Specification specification = (Specification) (root, query, criteriaBuilder) -> {
 Predicate predicate = criteriaBuilder.conjunction();
 if(!StringUtils.isEmpty(logDto.getTemplateName())){
 predicate.getExpressions().add( criteriaBuilder.like(root.get("templateName"),logDto.getTemplateName()));
 }
 if(logDto.getStartTime() !=null){
 predicate.getExpressions().add(criteriaBuilder.greaterThanOrEqualTo(root.get("createTime").as(Timestamp.class),logDto.getStartTime()));
 }
 query.where(predicate);
 return predicate;
 };
 return findAll(specification, pageable);
 }
}

通过继承BaseJpaRepository,使SendLogJpaRepository拥有了JpaRepository、JpaSpecificationExecutor接口中定义的所有方法功能。而且基于抽象基类中EntityManager实例,也可以非常方便的编写HQL和原生SQL查询等。最赏心悦目的是不仅拥有了最基本的CURD等功能,而且超复杂的分页查询也不分家了。只是JpaSpecification查询方式还不是特别出彩,下面继续最佳实践

二、集成QUERYDSL结构化查询

Querydsl是一个框架,可通过其流畅的API来构造静态类型的类似SQL的查询。这是Spring Data Jpa文档中对QueryDsl的描述。Spring Data Jpa对QueryDsl的扩展支持的比较好,基本可以无缝集成使用。Querydsl定义了一套和JpaSpecification类似的接口,使用方式上也类似,由于QueryDsl多了一个maven插件,可以在编译期间生成领域对象操作实体,所以在拼接复杂的查询条件时相比较JpaSpecification显的更灵活好用,特别在关联到多表查询的时候。下面看下怎么集成:

1、快速集成

因为之前有写过最简单的QueryDsl集成方式,所以这里就不在赘述了,具体参见《Querydsl结构化查询之jpa》,

2、丰富BaseJpaRepository基类

/**
 * @author: kl @kailing.pub
 * @date: 2019/11/8
 */
public abstract class BaseJpaRepository extends SimpleJpaRepository {

 public EntityManager em;
 protected final QuerydslJpaPredicateExecutorjpaPredicateExecutor;

 BaseJpaRepository(ClassdomainClass, EntityManager em) {
 super(domainClass, em);
 this.em = em;
 this.jpaPredicateExecutor = new QuerydslJpaPredicateExecutor<>(JpaEntityInformationSupport.getEntityInformation(domainClass, em), em, SimpleEntityPathResolver.INSTANCE, getRepositoryMethodMetadata());
 }
}

在BaseJpaRepository基类中新增了QuerydslJpaPredicateExecutor实例,它是Spring Data Jpa基于QueryDsl的一个实现。用来执行QueryDsl的Predicate相关查询。集成QueryDsl后,复杂分页查询的画风就变的更加清爽了,如:

 /**
 * QSendLog实体是QueryDsl插件自动生成的,插件会自动扫描加了@Entity的实体,生成一个用于查询的EntityPath类
 */
 private final static QSendLog sendLog = QSendLog.sendLog;

 public PagefindAll(SendLogDto logDto, Pageable pageable) {
 BooleanExpression expression = sendLog.isNotNull();
 if (logDto.getStartTime() != null) {
 expression = expression.and(sendLog.createTime.gt(logDto.getStartTime()));
 }
 if (!StringUtils.isEmpty(logDto.getTemplateName())) {
 expression = expression.and(sendLog.templateName.like("%"+logDto.getTemplateName()+"%"));
 }
 return jpaPredicateExecutor.findAll(expression, pageable);
 }

到目前为止,实现相同的复杂分页查询,代码已经非常的清爽和优雅了,在复杂的查询在这种模式下也变的非常的清晰。但是,这还不是十分完美的。还有两个问题需要解决下:

  • QuerydslJpaPredicateExecutor实现的方法不支持分页查询同时又有字段排序。下面是它的接口定义,可以看到,要么分页查询一步到位但是没有排序,要么排序查询返回List列表自己封装分页。


public interface QuerydslPredicateExecutor{
	OptionalfindOne(Predicate predicate);
	IterablefindAll(Predicate predicate);
	IterablefindAll(Predicate predicate, Sort sort);
	IterablefindAll(Predicate predicate, OrderSpecifier... orders);
	IterablefindAll(OrderSpecifier... orders);
	PagefindAll(Predicate predicate, Pageable pageable);
	long count(Predicate predicate);
	boolean exists(Predicate predicate);
}
  • 复杂的多表关联查询QuerydslJpaPredicateExecutor不支持

3、最终的BaseJpaRepository形态

Spring Data Jpa对QuerDsl的支持毕竟有限,但是QueryDsl是有这种功能的,像上面的场景就需要特别处理了。最终改造的BaseJpaRepository如下:

/**
 * @author: kl @kailing.pub
 * @date: 2019/11/8
 */
public abstract class BaseJpaRepository extends SimpleJpaRepository {

 protected final JPAQueryFactory jpaQueryFactory;
 protected final QuerydslJpaPredicateExecutorjpaPredicateExecutor;
 protected final EntityManager em;
 private final EntityPathpath;
 protected final Querydsl querydsl;
 
 BaseJpaRepository(ClassdomainClass, EntityManager em) {
 super(domainClass, em);
 this.em = em;
 this.jpaPredicateExecutor = new QuerydslJpaPredicateExecutor<>(JpaEntityInformationSupport.getEntityInformation(domainClass, em), em, SimpleEntityPathResolver.INSTANCE, getRepositoryMethodMetadata());
 this.jpaQueryFactory = new JPAQueryFactory(em);
 this.path = SimpleEntityPathResolver.INSTANCE.createPath(domainClass);
 this.querydsl = new Querydsl(em, new PathBuilder(path.getType(), path.getMetadata()));
 }
 
 protected PagefindAll(Predicate predicate, Pageable pageable, OrderSpecifier... orders) {
 final JPAQuery countQuery = jpaQueryFactory.selectFrom(path);
 countQuery.where(predicate);
 JPQLQueryquery = querydsl.applyPagination(pageable, countQuery);
 query.orderBy(orders);
 return PageableExecutionUtils.getPage(query.fetch(), pageable, countQuery::fetchCount);
 }
}

新增了findAll(Predicate predicate, Pageable pageable, OrderSpecifier... orders)方法,用于支持复杂分页查询的同时又有字段排序的查询场景。其次的改动是引入了JPAQueryFactory实例,用于多表关联的复杂查询。使用方式如下:

 /**
 * QSendLog实体是QueryDsl插件自动生成的,插件会自动扫描加了@Entity的实体,生成一个用于查询的EntityPath类
 */
 private final static QSendLog qSendLog = QSendLog.sendLog;
 private final static QTemplate qTemplate = QTemplate.template;

 public PagefindAll(SendLogDto logDto, Template template, Pageable pageable) {
 JPAQuery countQuery = jpaQueryFactory.selectFrom(qSendLog).leftJoin(qTemplate);
 countQuery.where(qSendLog.templateCode.eq(qTemplate.code));
 if(!StringUtils.isEmpty(template.getName())){
 countQuery.where(qTemplate.name.eq(template.getName()));
 }
 JPQLQuery query = querydsl.applyPagination(pageable, countQuery);
 return PageableExecutionUtils.getPage(query.fetch(), pageable, countQuery::fetchCount);
 }

三、集成P6SPY打印执行的SQL

上面的功能以及十分完美了,但是谈到最佳实践似乎少了一个打印SQL的功能。在使用Jpa的结构化语义构建复杂查询时,经常会因为各种原因导致查询的结果集不是自己想要的,但是又没法排查,因为不知道最终执行的sql是怎么样的。Spring Data Jpa也有打印sql的功能,但是比较鸡肋,它打印的是没有替换查询参数的sql,没法直接复制执行。所以这里推荐一个工具p6spy,p6spy是一个打印最终执行sql的工具,而且可以记录sql的执行耗时。使用起来也比较方便,简单三步集成:

  • 1、引入依赖
 <dependency>
 <groupId>p6spy</groupId>
 <artifactId>p6spy</artifactId>
 <version>${p6spy.version}</version>
 </dependency>
  • 2、修改数据源链接字符串
jdbc:mysql://127.0.0.1:3306 改成 jdbc:p6spy:mysql://127.0.0.1:3306
  • 3、添加配置spy.propertis配置
appender=com.p6spy.engine.spy.appender.Slf4JLogger
logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat
customLogMessageFormat = executionTime:%(executionTime)| sql:%(sqlSingleLine)

这个是最简化的自定义打印的配置,更多配置可参考:https://p6spy.readthedocs.io/en/latest/configandusage.html

结语

最后的BaseJpaRepository功能上基本满足了所有的查询需求,又做了基础查询和复杂查询的不分离,不至于把大量的复杂查询拼接逻辑写到service层,或者是新建的复杂查询类里。彻底解决了文首提出的那些问题。基于QueryDsl的复杂查询代码逻辑清晰,结构优雅,极力推荐使用。最后,在安利下p6spy,一个非常实用的打印sql的工具,可以帮助排查分析JPA最终生成执行的sql语句,其打印的sql语句可以直接复制到mysql管理工具中执行的。

相关推荐

团队管理“布阵术”:3招让你的团队战斗力爆表!

为何古代军队能够以一当十?为何现代企业有的团队高效似“特种部队”,有的却松散若“游击队”?**答案正隐匿于“布阵术”之中!**今时今日,让我们从古代兵法里萃取3个核心要义,助您塑造一支战斗力爆棚的...

知情人士回应字节大模型团队架构调整

【知情人士回应字节大模型团队架构调整】财联社2月21日电,针对原谷歌DeepMind副总裁吴永辉加入字节跳动后引发的团队调整问题,知情人士回应称:吴永辉博士主要负责AI基础研究探索工作,偏基础研究;A...

豆包大模型团队开源RLHF框架,训练吞吐量最高提升20倍

强化学习(RL)对大模型复杂推理能力提升有关键作用,但其复杂的计算流程对训练和部署也带来了巨大挑战。近日,字节跳动豆包大模型团队与香港大学联合提出HybridFlow。这是一个灵活高效的RL/RL...

创业团队如何设计股权架构及分配(创业团队如何设计股权架构及分配方案)

创业团队的股权架构设计,决定了公司在随后发展中呈现出的股权布局。如果最初的股权架构就存在先天不足,公司就很难顺利、稳定地成长起来。因此,创业之初,对股权设计应慎之又慎,避免留下巨大隐患和风险。两个人如...

消息称吴永辉入职后引发字节大模型团队架构大调整

2月21日,有消息称前谷歌大佬吴永辉加入字节跳动,并担任大模型团队Seed基础研究负责人后,引发了字节跳动大模型团队架构大调整。多名原本向朱文佳汇报的算法和技术负责人开始转向吴永辉汇报。简单来说,就是...

31页组织效能提升模型,经营管理团队搭建框架与权责定位

分享职场干货,提升能力!为职场精英打造个人知识体系,升职加薪!31页组织效能提升模型如何拿到分享的源文件:请您关注本头条号,然后私信本头条号“文米”2个字,按照操作流程,专人负责发送源文件给您。...

异形柱结构(异形柱结构技术规程)

下列关于混凝土异形柱结构设计的说法,其中何项正确?(A)混凝土异形柱框架结构可用于所有非抗震和抗震设防地区的一般居住建筑。(B)抗震设防烈度为6度时,对标准设防类(丙类)采用异形柱结构的建筑可不进行地...

职场干货:金字塔原理(金字塔原理实战篇)

金字塔原理的适用范围:金字塔原理适用于所有需要构建清晰逻辑框架的文章。第一篇:表达的逻辑。如何利用金字塔原理构建基本的金字塔结构受众(包括读者、听众、观众或学员)最容易理解的顺序:先了解主要的、抽象的...

底部剪力法(底部剪力法的基本原理)

某四层钢筋混凝土框架结构,计算简图如图1所示。抗震设防类别为丙类,抗震设防烈度为8度(0.2g),Ⅱ类场地,设计地震分组为第一组,第一自振周期T1=0.55s。一至四层的楼层侧向刚度依次为:K1=1...

结构等效重力荷载代表值(等效重力荷载系数)

某五层钢筋混凝土框架结构办公楼,房屋高度25.45m。抗震设防烈度8度,设防类别丙类,设计基本地震加速度0.2g,设计地震分组第二组,场地类别为Ⅱ类,混凝土强度等级C30。该结构平面和竖向均规则。假定...

体系结构已成昭告后世善莫大焉(体系构架是什么意思)

实践先行也理论已初步完成框架结构留余后人后世子孙俗话说前人栽树后人乘凉在夏商周大明大清民国共和前人栽树下吾之辈已完成结构体系又俗话说青出于蓝而胜于蓝各个时期任务不同吾辈探索框架结构体系经历有限肯定发展...

框架柱抗震构造要求(框架柱抗震设计)

某现浇钢筋混凝土框架-剪力墙结构高层办公楼,抗震设防烈度为8度(0.2g),场地类别为Ⅱ类,抗震等级:框架二级,剪力墙一级,混凝土强度等级:框架柱及剪力墙C50,框架梁及楼板C35,纵向钢筋及箍筋均采...

梁的刚度、挠度控制(钢梁挠度过大会引起什么原因)

某办公楼为现浇钢筋混凝土框架结构,r0=1.0,混凝土强度等级C35,纵向钢筋采用HRB400,箍筋采用HPB300。其二层(中间楼层)的局部平面图和次梁L-1的计算简图如图1~3(Z)所示,其中,K...

死要面子!有钱做大玻璃窗,却没有钱做“柱和梁”,不怕房塌吗?

活久见,有钱做2层落地大玻璃窗,却没有钱做“柱子和圈梁”,这样的农村自建房,安全吗?最近刷到个魔幻施工现场,如下图,这栋5开间的农村自建房,居然做了2个全景落地窗仔细观察,这2个落地窗还是飘窗,为了追...

不是承重墙,物业也不让拆?话说装修就一定要拆墙才行么

最近发现好多朋友装修时总想拆墙“爆改”空间,别以为只要避开承重墙就能随便砸!我家楼上邻居去年装修,拆了阳台矮墙想扩客厅,结果物业直接上门叫停。后来才知道,这种配重墙拆了会让阳台承重失衡,整栋楼都可能变...

取消回复欢迎 发表评论: