javascript
Spring Data JPA 从入门到精通~QueryByExampleExecutor的使用
?QueryByExampleExecutor 的使用
按示例查詢(QBE)是一種用戶友好的查詢技術(shù),具有簡單的接口,它允許動(dòng)態(tài)查詢創(chuàng)建,并且不需要編寫包含字段名稱的查詢。從 UML 圖中,可以看出繼承 JpaRepository 接口后,自動(dòng)擁有了按“實(shí)例”進(jìn)行查詢的諸多方法,可見 Spring Data 的團(tuán)隊(duì)已經(jīng)認(rèn)為了 QBE 是 Spring JPA 的基本功能了,繼承 QueryByExampleExecutor 和繼承 JpaRepository 都會(huì)有這些基本方法,所以 QueryByExampleExecutor 位于 Spring Data Common 中,而 JpaRepository 位于 Spring Data JPA 中。
QueryByExampleExecutor 詳細(xì)配置
先來看一下 QueryByExampleExecutor 的源碼:
public interface QueryByExampleExecutor<T> { //根據(jù)“實(shí)例”查找一個(gè)對(duì)象。 <S extends T> S findOne(Example<S> example); //根據(jù)“實(shí)例”查找一批對(duì)象 <S extends T> Iterable<S> findAll(Example<S> example); //根據(jù)“實(shí)例”查找一批對(duì)象,且排序 <S extends T> Iterable<S> findAll(Example<S> example, Sort sort);//根據(jù)“實(shí)例”查找一批對(duì)象,且排序和分頁 <S extends T> Page<S> findAll(Example<S> example, Pageable pageable); //根據(jù)“實(shí)例”查找,返回符合條件的對(duì)象個(gè)數(shù) <S extends T> long count(Example<S> example); //根據(jù)“實(shí)例”判斷是否有符合條件的對(duì)象 <S extends T> boolean exists(Example<S> example); }從源碼上可以看出,只要了解 Example 基本上就可以掌握它的用法和 API 了。
public class Example<T> {@NonNullprivate final T probe;@NonNullprivate final ExampleMatcher matcher; public static <T> Example<T> of(T probe) {return new Example(probe, ExampleMatcher.matching()); } public static <T> Example<T> of(T probe, ExampleMatcher matcher) {return new Example(probe, matcher); } ...... }我們從源碼中可以看出 Example 主要包含三部分內(nèi)容。
- Probe:這是具有填充字段的域?qū)ο蟮膶?shí)際實(shí)體類,即查詢條件的封裝類(又可以理解為:查詢條件參數(shù)),必填。
- ExampleMatcher:ExampleMatcher 有關(guān)于如何匹配特定字段的匹配規(guī)則,它可以重復(fù)使用在多個(gè)示例,必填;如果不填,用默認(rèn)的(又可以理解為參數(shù)的匹配規(guī)則)。
- Example:Example 由 Probe 探針和 ExampleMatcher 組成,它用于創(chuàng)建查詢,即組合查詢參數(shù)和參數(shù)的匹配規(guī)則。
QueryByExampleExecutor 的使用案例
//創(chuàng)建查詢條件數(shù)據(jù)對(duì)象 Customer customer = new Customer(); customer.setName("Jack"); customer.setAddress("上海"); //創(chuàng)建匹配器,即如何使用查詢條件 ExampleMatcher matcher = ExampleMatcher.matching() //構(gòu)建對(duì)象.withMatcher("name", GenericPropertyMatchers.startsWith()) //姓名采用“開始匹配”的方式查詢.withIgnorePaths("focus"); //忽略屬性:是否關(guān)注。因?yàn)槭腔绢愋?#xff0c;需要忽略掉 //創(chuàng)建實(shí)例 Example<Customer> ex = Example.of(customer, matcher); //查詢 List<Customer> ls = dao.findAll(ex); //輸出結(jié)果 for (Customer bo:ls) {System.out.println(bo.getName()); }上面例子中,是這樣創(chuàng)建“實(shí)例”的:
Example<Customer> ex = Example.of(customer, matcher);
可以看到,Example 對(duì)象由 customer 和 matcher 共同創(chuàng)建,為講解方便,再來結(jié)合案例先來明確一些定義。
- Probe:實(shí)體對(duì)象,在持久化框架中與 Table 對(duì)應(yīng)的域?qū)ο?#xff0c;一個(gè)對(duì)象代表數(shù)據(jù)庫表中的一條記錄,如上例中 Customer 對(duì)象。在構(gòu)建查詢條件時(shí),一個(gè)實(shí)體對(duì)象代表的是查詢條件中的“數(shù)值”部分,如要查詢姓“Jack”的客戶,實(shí)體對(duì)象只能存儲(chǔ)條件值“Jack”。
- ExampleMatcher:匹配器,它是匹配“實(shí)體對(duì)象”的,表示了如何使用“實(shí)體對(duì)象”中的“值”進(jìn)行查詢,它代表的是“查詢方式”,解釋了如何去查的問題。例如,要查詢姓“劉”的客戶,即姓名以“劉”開頭的客戶,該對(duì)象就表示了“以某某開頭的”這個(gè)查詢方式,如上例中 withMatcher("name", GenericPropertyMatchers.startsWith())。
- Example:實(shí)例對(duì)象,代表的是完整的查詢條件。由實(shí)體對(duì)象(查詢條件值)和匹配器(查詢方式)共同創(chuàng)建。
再來理解“實(shí)例查詢”,顧名思義,就是通過一個(gè)例子來查詢,要查詢的是 Customer 對(duì)象,查詢條件也是一個(gè) Customer 對(duì)象,通過一個(gè)現(xiàn)有的客戶對(duì)象作為例子,查詢和這個(gè)例子相匹配的對(duì)象。
QueryByExampleExecutor 的特點(diǎn)及約束
- 支持動(dòng)態(tài)查詢:即支持查詢條件個(gè)數(shù)不固定的情況,如客戶列表中有多個(gè)過濾條件,用戶使用時(shí)在“地址”查詢框中輸入了值,就需要按地址進(jìn)行過濾,如果沒有輸入值,就忽略這個(gè)過濾條件。對(duì)應(yīng)的實(shí)現(xiàn)是,在構(gòu)建查詢條件 Customer 對(duì)象時(shí),將 address 屬性值置具體的條件值或置為 null。
- 不支持過濾條件分組:即不支持過濾條件用 or(或)來連接,所有的過濾查件,都是簡單一層的用 and(并且)連接,如 firstname = ?0 or (firstname = ?1 and lastname = ?2)。
- 僅支持字符串的開始/包含/結(jié)束/正則表達(dá)式匹配和其他屬性類型的精確匹配。查詢時(shí),對(duì)一個(gè)要進(jìn)行匹配的屬性(如:姓名 name),只能傳入一個(gè)過濾條件值,如以 Customer 為例,要查詢姓“劉”的客戶,“劉”這個(gè)條件值就存儲(chǔ)在表示條件對(duì)象的 Customer 對(duì)象的 name 屬性中,針對(duì)于“姓名”的過濾也只有這么一個(gè)存儲(chǔ)過濾值的位置,沒辦法同時(shí)傳入兩個(gè)過濾值。正是由于這個(gè)限制,有些查詢是沒辦法支持的,例如要查詢某個(gè)時(shí)間段內(nèi)添加的客戶,對(duì)應(yīng)的屬性是 addTime,需要傳入“開始時(shí)間”和“結(jié)束時(shí)間”兩個(gè)條件值,而這種查詢方式?jīng)]有存兩個(gè)值的位置,所以就沒辦法完成這樣的查詢。
ExampleMatcher 源碼解讀
(1)源碼解讀
public class ExampleMatcher { NullHandler nullHandler; StringMatcher defaultStringMatcher; //默認(rèn) boolean defaultIgnoreCase; //默認(rèn)大小寫忽略方式 PropertySpecifiers propertySpecifiers; //各屬性特定查詢方式 Set<String> ignoredPaths; //忽略屬性列表 //Null值處理方式,通過構(gòu)造方法,我們發(fā)現(xiàn)默認(rèn)忽略NullHandler nullHandler;//字符串匹配方式,通過構(gòu)造方法可以看出默認(rèn)是DEFAULT(默認(rèn),效果同EXACT),EXACT(相等)StringMatcher defaultStringMatcher;//各屬性特定查詢方式,默認(rèn)無特殊指定的。PropertySpecifiers propertySpecifiers;//忽略屬性列表,默認(rèn)無。Set<String> ignoredPaths;//大小寫忽略方式,默認(rèn)不忽略。boolean defaultIgnoreCase;@Wither(AccessLevel.PRIVATE) MatchMode mode; //通用、內(nèi)部、默認(rèn)構(gòu)造方法。private ExampleMatcher() {this(NullHandler.IGNORE, StringMatcher.DEFAULT, new PropertySpecifiers(), Collections.<String>emptySet(), false,MatchMode.ALL);}//Example的默認(rèn)匹配方式public static ExampleMatcher matching() {return matchingAll();} public static ExampleMatcher matchingAll() {return new ExampleMatcher().withMode(MatchMode.ALL); } ...... }(2)關(guān)鍵屬性分析
- nullHandler:Null 值處理方式,枚舉類型,有兩個(gè)可選值,INCLUDE(包括)、IGNORE(忽略)。
-
- 標(biāo)識(shí)作為條件的實(shí)體對(duì)象中,一個(gè)屬性值(條件值)為 Null 時(shí),是否參與過濾;
- 當(dāng)該選項(xiàng)值是 INCLUDE 時(shí),表示仍參與過濾,會(huì)匹配數(shù)據(jù)庫表中該字段值是 Null 的記錄;
- 若為 IGNORE 值,表示不參與過濾。
- defaultStringMatcher:默認(rèn)字符串匹配方式,枚舉類型,有 6 個(gè)可選值,DEFAULT(默認(rèn),效果同 EXACT)、EXACT(相等)、STARTING(開始匹配)、ENDING(結(jié)束匹配)、CONTAINING(包含,模糊匹配)、REGEX(正則表達(dá)式)。
-
- 該配置對(duì)所有字符串屬性過濾有效,除非該屬性在 propertySpecifiers 中單獨(dú)定義自己的匹配方式。
- defaultIgnoreCase:默認(rèn)大小寫忽略方式,布爾型,當(dāng)值為 false 時(shí),即不忽略,大小不相等。
-
- 該配置對(duì)所有字符串屬性過濾有效,除非該屬性在 propertySpecifiers 中單獨(dú)定義自己的忽略大小寫方式。
- propertySpecifiers:各屬性特定查詢方式,描述了各個(gè)屬性單獨(dú)定義的查詢方式,每個(gè)查詢方式中包含4個(gè)元素:屬性名、字符串匹配方式、大小寫忽略方式、屬性轉(zhuǎn)換器。
-
- 如果屬性未單獨(dú)定義查詢方式,或單獨(dú)查詢方式中,某個(gè)元素未定義(如字符串匹配方式),則采用 ExampleMatcher 中定義的默認(rèn)值,即上面介紹的 defaultStringMatcher 和 defaultIgnoreCase 的值。
- ignoredPaths:忽略屬性列表,忽略的屬性不參與查詢過濾。
(3)字符串匹配舉例
| 字符串匹配方式 | 對(duì)應(yīng)JPQL的寫法 |
| Default&不忽略大小寫 | firstname=?1 |
| Exact&忽略大小寫 | LOWER(firstname) = LOWER(?1) |
| Starting&忽略大小寫 | LOWER(firstname) like LOWER(?0)+'%' |
| Ending&不忽略大小寫 | firstname like '%'+?1 |
| Containing不忽略大小寫 | firstname like '%'+?1+'%' |
QueryByExampleExecutor 使用場(chǎng)景 & 實(shí)際的使用
使用場(chǎng)景
使用一組靜態(tài)或動(dòng)態(tài)約束來查詢數(shù)據(jù)存儲(chǔ)、頻繁重構(gòu)域?qū)ο?#xff0c;而不用擔(dān)心破壞現(xiàn)有查詢、簡單的查詢的使用場(chǎng)景,有時(shí)候還是挺方便的。
實(shí)際使用中我們需要考慮的因素
查詢條件的表示,有兩部分,一是條件值,二是查詢方式。條件值用實(shí)體對(duì)象(如 Customer 對(duì)象)來存儲(chǔ),相對(duì)簡單,當(dāng)頁面?zhèn)魅脒^濾條件值時(shí),存入相對(duì)應(yīng)的屬性中,沒入傳入時(shí),屬性保持默認(rèn)值。查詢方式是用匹配器 ExampleMatcher 來表示,情況相對(duì)復(fù)雜些,需要考慮的因素有以下幾個(gè):
(1)Null 值的處理
當(dāng)某個(gè)條件值為 Null時(shí),是應(yīng)當(dāng)忽略這個(gè)過濾條件呢,還是應(yīng)當(dāng)去匹配數(shù)據(jù)庫表中該字段值是 Null 的記錄?
Null 值處理方式:默認(rèn)值是 IGNORE(忽略),即當(dāng)條件值為 Null 時(shí),則忽略此過濾條件,一般業(yè)務(wù)也是采用這種方式就可滿足。當(dāng)需要查詢數(shù)據(jù)庫表中屬性為 Null 的記錄時(shí),可將值設(shè)為 INCLUDE,這時(shí),對(duì)于不需要參與查詢的屬性,都必須添加到忽略列表(ignoredPaths)中,否則會(huì)出現(xiàn)查不到數(shù)據(jù)的情況。
(2)基本類型的處理
如客戶 Customer 對(duì)象中的年齡 age 是 int 型的,當(dāng)頁面不傳入條件值時(shí),它默認(rèn)是0,是有值的,那是否參與查詢呢?
關(guān)于基本數(shù)據(jù)類型處理方式:實(shí)體對(duì)象中,避免使用基本數(shù)據(jù)類型,采用包裝器類型。如果已經(jīng)采用了基本類型,而這個(gè)屬性查詢時(shí)不需要進(jìn)行過濾,則把它添加到忽略列表(ignoredPaths)中。
(3)忽略某些屬性值
一個(gè)實(shí)體對(duì)象,有許多個(gè)屬性,是否每個(gè)屬性都參與過濾?是否可以忽略某些屬性?
ignoredPaths:雖然某些字段里面有值或者設(shè)置了其他匹配規(guī)則,只要放在 ignoredPaths 中,就會(huì)忽略此字段的,不作為過濾條件。
(4)不同的過濾方式
同樣是作為 String 值,可能“姓名”希望精確匹配,“地址”希望模糊匹配,如何做到?
默認(rèn)配置和特殊配置混合使用:默認(rèn)創(chuàng)建匹配器時(shí),字符串采用的是精確匹配、不忽略大小寫,可以通過操作方法改變這種默認(rèn)匹配,以滿足大多數(shù)查詢條件的需要,如將“字符串匹配方式”改為 CONTAINING(包含,模糊匹配),這是比較常用的情況。對(duì)于個(gè)別屬性需要特定的查詢方式,可以通過配置“屬性特定查詢方式”來滿足要求,設(shè)置 propertySpecifiers 的值即可。
(5)大小寫匹配
字符串匹配時(shí),有時(shí)可能希望忽略大小寫,有時(shí)則不忽略,如何做到?
defaultIgnoreCase:忽略大小的生效與否,是依賴于數(shù)據(jù)庫的。例如 MySQL 數(shù)據(jù)庫中,默認(rèn)創(chuàng)建表結(jié)構(gòu)時(shí),字段是已經(jīng)忽略大小寫的,所以這個(gè)配置與否,都是忽略的。如果業(yè)務(wù)需要嚴(yán)格區(qū)分大小寫,可以改變數(shù)據(jù)庫表結(jié)構(gòu)屬性來實(shí)現(xiàn)。
實(shí)際使用案例說明
(1)無匹配器的情況
- 要求:查詢地址是“河南省鄭州市”,且重點(diǎn)關(guān)注的客戶。
- 說明:使用默認(rèn)匹配器就可以滿足查詢條件,則不需要?jiǎng)?chuàng)建匹配器。
(2)多種條件組合
- 要求:根據(jù)姓名、地址、備注進(jìn)行模糊查詢,忽略大小寫,地址要求開始匹配。
- 說明:這是通用情況,主要演示改變默認(rèn)字符串匹配方式、改變默認(rèn)大小寫忽略方式、屬性特定查詢方式配置、忽略屬性列表配置。
(3)多級(jí)查詢
- 要求:查詢所有潛在客戶。
- 說明:主要演示多層級(jí)屬性查詢。
(4)查詢 Null 值
- 要求:地址是 Null 的客戶。
- 說明:主要演示改變“Null 值處理方式”。
(5)雖然我們工作中用的最多的還是“簡單查詢”(因?yàn)楹唵?#xff0c;所以…)和基于 JPA Criteria 的動(dòng)態(tài)查詢(可以滿足所有需求,沒有局限性)。
但是 QueryByExampleExecutor 還是個(gè)非常不錯(cuò)的兩種中間場(chǎng)景的查詢處理手段,其他人沒有用,感覺是對(duì)其不熟悉,還是希望我們學(xué)習(xí)過 QueryByExampleExecutor 的開發(fā)者用起來,用熟悉了會(huì)增加開發(fā)效率。
QueryByExampleExecutor 的實(shí)現(xiàn)源碼
(1)我們通過開發(fā)工具——Hierarchy,來看一下其接口的實(shí)現(xiàn)類有哪些:
(2)我們發(fā)現(xiàn) JpaSpecificationExecutor 的實(shí)現(xiàn)類是 SimpleJpaRepository。
而 SimpleJpaRepository 也實(shí)現(xiàn)了 JpaSpecificationExecutor,于是就利用 Specification 的特性,創(chuàng)建了內(nèi)部類 ExampleSpecification,通過 Exmaple 實(shí)現(xiàn)了一套工具類和對(duì) Predicate 的構(gòu)建,進(jìn)而實(shí)現(xiàn)了整個(gè) ExampleQuery 的邏輯。
如果我們自己去看源碼,會(huì)發(fā)現(xiàn) QueryByExampleExecutor 給我們提供了兩種思路:
- 通過 JpaSpecificationExecutor 自定義 Response 的思路。
- 和對(duì) JpaSpecificationExecutor 的擴(kuò)展思路(在后面章節(jié)自定義 Repository 中會(huì)詳細(xì)介紹)。
(3)SimpleJpaRepository 實(shí)現(xiàn)類中的關(guān)鍵源碼:
public class SimpleJpaRepository<T, ID extends Serializable>implements JpaRepository<T, ID>, JpaSpecificationExecutor<T> {private final EntityManager em; public <S extends T> S findOne(Example<S> example) {try {return getQuery(new ExampleSpecification<S>(example), example.getProbeType(), (Sort) null).getSingleResult();} catch (NoResultException e) {return null;} } protected <S extends T> TypedQuery<S> getQuery(Specification<S> spec, Class<S> domainClass, Sort sort) {CriteriaBuilder builder = em.getCriteriaBuilder();CriteriaQuery<S> query = builder.createQuery(domainClass);Root<S> root = applySpecificationToCriteria(spec, domainClass, query);query.select(root);if (sort != null) {query.orderBy(toOrders(sort, root, builder));}return applyRepositoryMethodMetadata(em.createQuery(query)); } ...... }(4)讀 SimpleJpaRepository 源碼給大家的啟示
當(dāng)我們學(xué)習(xí)一個(gè) API 的時(shí)候最好順帶讀一下源碼,分析一下它的實(shí)現(xiàn)結(jié)構(gòu),這樣你會(huì)有意外發(fā)現(xiàn):
- 學(xué)習(xí)源碼的編程思想;
- 學(xué)習(xí)源碼的實(shí)現(xiàn)方式,我自己如何寫,這樣可以很快的提高我們自己的編程水平。
總結(jié)
以上是生活随笔為你收集整理的Spring Data JPA 从入门到精通~QueryByExampleExecutor的使用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux date命令显示毫秒,解决M
- 下一篇: Spring Data JPA 从入门到