<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32: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); }
優點:
缺陷:
缺陷就一條,這種擴充套件介面的方式要實現複雜的分頁查詢,有兩種方式,而且這兩種方式程式碼寫起來都不怎麼優雅,而且會把大量的條件拼接邏輯寫在呼叫查詢的service層。
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了,所以這種方式不合格直接上黑名單了。
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是怎麼做到繼承一個介面就能實現各種複雜查詢的呢?這裡其實是一個典型的代理模式的應用,只要繼承了最底層的Repository介面,在應用啟動時就會幫你生成一個代理範例,而真正的目標類才是最終執行查詢的類,這個類就是:SimpleJpaRepository,它實現了JpaRepository、JpaSpecificationExecutor的所有介面,所以只要基於SimpleJpaRepository客製化Repository基礎類別,就能擁有繼承介面一樣的查詢功能,而且可以在實現類裡編寫複雜的查詢方法了。
/** * @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是一個框架,可通過其流暢的API來構造靜態型別的類似SQL的查詢。這是Spring Data Jpa檔案中對QueryDsl的描述。Spring Data Jpa對QueryDsl的擴充套件支援的比較好,基本可以無縫整合使用。Querydsl定義了一套和JpaSpecification類似的介面,使用方式上也類似,由於QueryDsl多了一個maven外掛,可以在編譯期間生成領域物件操作實體,所以在拼接複雜的查詢條件時相比較JpaSpecification顯的更靈活好用,特別在關聯到多表查詢的時候。下面看下怎麼整合:
因為之前有寫過最簡單的QueryDsl整合方式,所以這裡就不在贅述了,具體參見《Querydsl結構化查詢之jpa》,
/** * @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); }
到目前為止,實現相同的複雜分頁查詢,程式碼已經非常的清爽和優雅了,在複雜的查詢在這種模式下也變的非常的清晰。但是,這還不是十分完美的。還有兩個問題需要解決下:
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); }
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); }
上面的功能以及十分完美了,但是談到最佳實踐似乎少了一個列印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管理工具中執行的。
以上就是Spring Data Jpa框架最佳實踐範例的詳細內容,更多關於Spring Data Jpa框架的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45