Spring Data Jpa框架最佳实践示例
作者:kl 发布时间:2021-11-25 00:43:01
前言
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
来源:http://www.kailing.pub/article/index/arcid/263.html
猜你喜欢
- 一、目的本篇文章的目的是记录本人使用flutter加载与调用第三方aar包。二、背景本人go后端,业余时间喜欢玩玩flutter。一直有一个
- 一、问题描述上周不是搭了个SpringBoot整合sharding-jdbc分库分表的架子么,组里老哥不让我把开发环境的配置文件放到reso
- 一、Monkey 是什么?Monkey 就是SDK中附带的一个工具。二、Monkey 测试的目的?:该工具用于进行压力测试。 然后开发人员结
- 概述从今天开始, 小白我将带大家开启 Jave 数据结构 & 算法的新篇章.循环队列循环队列 (Circular Queue) 是一
- 这篇文章主要介绍了springboot如何使用AOP做访问请求日志,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价
- Solr我还是个菜鸟,写这一些文章只是记录一下最近一段时间学习Solr的心得。 Solr是什么? 最近我学Solr的时候,一直看到一句话,S
- WHY朋友在群里求助一个问题,问题原型是这样的:String str = "{{10.14, 11.24, 44.55, 41.0
- 前情提要我们上节内容学习了如何创建\注册\读取bean我们发现bean对象操作十分的繁琐!所以我们这个章节,就带大家来了解更加简单的bean
- 关于base64编码Encode和Decode编码的几种方式Base64是一种能将任意Binary资料用64种字元组合成字串的方法,而这个B
- Java有四种访问权限,其中三种有访问权限修饰符,分别为private,public和protected,还有一种不带任何修饰符:1.&nb
- 1.将本地jar包放入本地仓库。只需执行如下命令即可:mvn install:install-file -Dfile=D:/demo/fib
- 1.问题描述汉诺塔问题是一个经典的问题。汉诺塔(Hanoi Tower),又称河内塔,源于印度一个古老传说。大梵天创造世界的时候做了三根金刚
- 本文实例讲述了Android之复选框对话框用法。分享给大家供大家参考。具体如下:main.xml布局文件<?xml version=&
- 循环依赖定义循环依赖就 循环引用,就是两个或多个 bean 相互之间的持有对方,比如 CircleA 引用 CircleB , Circle
- 在JSP里,获取客户端的IP地址的方法是:request.getRemoteAddr(),这种方法在大部分情况下都是有效的。但是在通过了Ap
- spring boot 使用profile来分区配置很多时候,我们项目在开发环境和生成环境的环境配置是不一样的,例如,数据库配置,在开发的时
- 十六进制字符串与数值类型之间转换(C# 编程指南) 以下示例演示如何执行下列任务: 获取字符串中每个字符的十六进制值。 获取与十六进制字符串
- ELK环境安装ELK是指Elasticsearch、Kibana、Logstash这三种服务搭建的日志收集系统,具体搭建方式可以参考《Spr
- 假设下面是你的视频网站链接列表,如果别人想爬取你的数据十分轻松,看规则就知道数据库是序列自增的http://www.xxxx.com/vid
- 背景数据之间两两趋势比较在数据分析应用中是非常常见的应用场景,如下所示:模拟考批次班级学生语文数学英语202302三年一班张小明130145