Spring?Data?Jpa框架最佳實踐示例
前言
Spring Data Jpa框架的目標是顯著減少實現(xiàn)各種持久性存儲的數(shù)據(jù)訪問層所需的樣板代碼量。
Spring Data Jpa存儲庫抽象中的中央接口是Repository。它需要領域實體類以及領域實體ID類型作為類型參數(shù)來進行管理。該接口主要用作標記接口,以捕獲要使用的類型并幫助您發(fā)現(xiàn)擴展該接口的接口。
CrudRepository、JpaRepository是更具體的數(shù)據(jù)操作抽象,一般我們在項目中使用的時候定義我們的領域接口然后繼承CrudRepository或JpaRepository即可實現(xiàn)實現(xiàn)基礎的CURD方法了,但是這種用法有局限性,不能處理超復雜的查詢,而且稍微復雜的查詢代碼寫起來也不是很優(yōu)雅,所以下面看看怎么最優(yōu)雅的解決這個問題。
擴展接口用法
/** * @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); }
優(yōu)點:
- 1、這種擴展接口的方式是最常見的用法,繼承JpaRepository接口后,立馬擁有基礎的CURD功能
- 2、還可以通過特定的方法名做解析查詢,這個可以算spring Data Jpa的最特殊的特性了。而且主流的IDE對這種使用方式都有比較好的自動化支持,在輸入要解析的方法名時會給出提示。
- 3、可以非常方便的以注解的形式支持HQL和原生SQL
缺陷:
- 1、復雜的分頁查詢支持不好
缺陷就一條,這種擴展接口的方式要實現(xiàn)復雜的分頁查詢,有兩種方式,而且這兩種方式代碼寫起來都不怎么優(yōu)雅,而且會把大量的條件拼接邏輯寫在調用查詢的service層。
- 第一種、實例查詢(Example Query)方式:
public void testExampleQuery() { SendLog log = new SendLog(); log.setTemplateName("kl"); /* * 注意:withMatcher方法的propertyPath參數(shù)值填寫領域對象的字段值,而不是實際的表字段 */ 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); }
上面代碼實現(xiàn)的語義是模糊查詢templateName等于"kl"的記錄并分頁,乍一看這個代碼還過得去哈,其實當查詢的條件多一點,這種代碼就會變得又臭又長,而且只支持基礎的字符串類型的字段查詢,如果查詢條件有時間篩選的話就不支持了,在復雜點多表關聯(lián)的話就更GG了,所以這種方式不合格直接上黑名單了。
- 第二種、繼承JpaSpecificationExecutor方式:
JPA 2引入了一個標準API,您可以使用它來以編程方式構建查詢。Spring Data JPA提供了使用JPA標準API定義此類規(guī)范的API。這種方式首先需要繼承JpaSpecificationExecutor接口,下面我們用這種方式實現(xiàn)和上面相同語義的查詢:
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是怎么做到繼承一個接口就能實現(xiàn)各種復雜查詢的呢?這里其實是一個典型的代理模式的應用,只要繼承了最底層的Repository接口,在應用啟動時就會幫你生成一個代理實例,而真正的目標類才是最終執(zhí)行查詢的類,這個類就是:SimpleJpaRepository,它實現(xiàn)了JpaRepository、JpaSpecificationExecutor的所有接口,所以只要基于SimpleJpaRepository定制Repository基類,就能擁有繼承接口一樣的查詢功能,而且可以在實現(xiàn)類里編寫復雜的查詢方法了。
一、繼承SIMPLEJPAREPOSITORY實現(xiàn)類
/** * @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的上下文中已經(jīng)有了,會自動注入。領域對象類型在具體的實現(xiàn)類中注入即可。如:
/** * @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 實現(xiàn)復雜分頁查詢 * @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查詢方式還不是特別出彩,下面繼續(xù)最佳實踐
二、集成QUERYDSL結構化查詢
Querydsl是一個框架,可通過其流暢的API來構造靜態(tài)類型的類似SQL的查詢。這是Spring Data Jpa文檔中對QueryDsl的描述。Spring Data Jpa對QueryDsl的擴展支持的比較好,基本可以無縫集成使用。Querydsl定義了一套和JpaSpecification類似的接口,使用方式上也類似,由于QueryDsl多了一個maven插件,可以在編譯期間生成領域對象操作實體,所以在拼接復雜的查詢條件時相比較JpaSpecification顯的更靈活好用,特別在關聯(lián)到多表查詢的時候。下面看下怎么集成:
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的一個實現(xiàn)。用來執(zhí)行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); }
到目前為止,實現(xiàn)相同的復雜分頁查詢,代碼已經(jīng)非常的清爽和優(yōu)雅了,在復雜的查詢在這種模式下也變的非常的清晰。但是,這還不是十分完美的。還有兩個問題需要解決下:
- QuerydslJpaPredicateExecutor實現(xiàn)的方法不支持分頁查詢同時又有字段排序。下面是它的接口定義,可以看到,要么分頁查詢一步到位但是沒有排序,要么排序查詢返回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); }
- 復雜的多表關聯(lián)查詢QuerydslJpaPredicateExecutor不支持
3、最終的BaseJpaRepository形態(tài)
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實例,用于多表關聯(lián)的復雜查詢。使用方式如下:
/** * 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打印執(zhí)行的SQL
上面的功能以及十分完美了,但是談到最佳實踐似乎少了一個打印SQL的功能。在使用Jpa的結構化語義構建復雜查詢時,經(jīng)常會因為各種原因導致查詢的結果集不是自己想要的,但是又沒法排查,因為不知道最終執(zhí)行的sql是怎么樣的。Spring Data Jpa也有打印sql的功能,但是比較雞肋,它打印的是沒有替換查詢參數(shù)的sql,沒法直接復制執(zhí)行。所以這里推薦一個工具p6spy,p6spy是一個打印最終執(zhí)行sql的工具,而且可以記錄sql的執(zhí)行耗時。使用起來也比較方便,簡單三步集成:
1、引入依賴
<dependency> <groupId>p6spy</groupId> <artifactId>p6spy</artifactId> <version>${p6spy.version}</version> </dependency>
2、修改數(shù)據(jù)源鏈接字符串
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的復雜查詢代碼邏輯清晰,結構優(yōu)雅,極力推薦使用。最后,在安利下p6spy,一個非常實用的打印sql的工具,可以幫助排查分析JPA最終生成執(zhí)行的sql語句,其打印的sql語句可以直接復制到mysql管理工具中執(zhí)行的。
以上就是Spring Data Jpa框架最佳實踐示例的詳細內容,更多關于Spring Data Jpa框架的資料請關注腳本之家其它相關文章!
相關文章
SpringBoot+SpringBatch+Quartz整合定時批量任務方式
這篇文章主要介紹了SpringBoot+SpringBatch+Quartz整合定時批量任務方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09Java ArrayList的基本概念和作用及動態(tài)數(shù)組的機制與性能
在Java中,ArrayList是一個實現(xiàn)了List接口的動態(tài)數(shù)組,它可以根據(jù)需要自動增加大小,因此可以存儲任意數(shù)量的元素,這篇文章主要介紹了探秘Java ArrayList的基本概念和作用及動態(tài)數(shù)組的機制與性能,需要的朋友可以參考下2023-12-12解決SpringMVC接收不到ajaxPOST參數(shù)的問題
今天小編就為大家分享一篇解決SpringMVC接收不到ajaxPOST參數(shù)的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-08-08基于RecyclerChart的KLine繪制Volume實現(xiàn)詳解
這篇文章主要為大家介紹了基于RecyclerChart的KLine繪制Volume實現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03SpringBoot如何通過webjars管理靜態(tài)資源文件夾
這篇文章主要介紹了SpringBoot如何通過webjars管理靜態(tài)資源文件夾,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-10-10關于servlet向mysql添加數(shù)據(jù)時中文亂碼問題的解決
最近在工作中遇到一個小問題,出現(xiàn)了中文亂碼的問題,無奈只能想辦法解決,下面這篇文章主要給大家介紹了關于servlet向mysql添加數(shù)據(jù)時中文亂碼問題的解決方法,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考借鑒,下面來一起看看吧。2017-08-08