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