Spring Boot JPA Repository之existsBy查詢方法失效的解決
引言: Spring Boot號稱微服務的利器,在結合瞭Spring Data與JPA之後,更是如虎添翼,開發快速的不像話,本文將講述一個關於JPA中一個詭異問題的診斷分析過程以及修復方法。
環境介紹
JDK 1.8 Spring 4.2 Spring Boot 1.5.9
問題描述
在Spring Data中的Repository接口中創建瞭一個檢查數據是否存在的接口方法:
@Repository public interface VideoEntityRepository extends JpaRepository<VideoEntity, Long> { ........ public boolean existsByUserIdAndName(long userId, String name); }
VideoEntity的類如下:
@SuppressWarnings("serial") @Table(name="flook_video") @Entity @Data @EqualsAndHashCode(callSuper=true) public class VideoEntity extends BaseEntity { @Column(name="user_id") private long userId; @Column(name="name") private String name; @Column(name="change_version") private double changeVersion; }
在調用方法existsBy方法的時候,返回的結果一直為false, 結果不正確,在偶的期望中,其執行結果應該不會出錯的?那問題出在哪裡呢?
關於existsBy的介紹
Spring Data提供瞭若幹非常實用的擴展,將數據庫表日常的CRUD操作都進行很好的實現,並提供瞭若幹擴展機制,基於一套簡單易用的命名規則,來基於聲明式實現場景的數據庫查詢操作:
countByColumName existsByColumnName
上述兩種方式都是由Spring Data來幫助動態生成SQL的。
基於@Query方式
除瞭基於countBy/existsBy兩種方式之外,可以直接使用@Query方式來標註特定的SQL或者JPQL來實現功能,由於這種方式需要些SQL,功能上完全覆蓋,但是工作量略大,不是最優的方式。
問題分析
由於底層使用瞭Hibernate/JPA/Spring Data來實現的數據訪問層的實現,所以,最好的方式當然是查看動態生成的SQL瞭,於是找到瞭生成的SQL語句:
select videoentit0_.id as col_0_0_ from video videoentit0_ where videoentit0_.user_id=? and videoentit0_.name=? limit ?
表的名稱是video,這個名稱是不對的。這個情況是怎麼發生的呢?
問題的解決
經過分析發現,是由於在代碼中存在兩個類名完全相同VideoEntity的類,雖然在Repository中我們的的確確沒有引用錯誤相關的,問題應該出在當Spring Data碰到兩個相同的類名之時,其實不知道如何來生成SQL的,換句話說,其應該是基於類名而非類路徑來動態生成執行SQL的。
TODO: 基於源代碼中找到相關部分內容。
問題解決
將當前的VideoEntity重新命名,確保不存在重名的問題,即使類的路徑不一樣,但是類名一樣,也是會產生這樣的問題。
從一個側面來分析,在Spring Data中所有的DataBean都是需要使用全路徑的類名的,否則同樣會出現問題。
JpaRepository 查詢規范
1.JpaRepository支持接口規范方法名查詢
意思是如果在接口中定義的查詢方法符合它的命名規則,就可以不用寫實現,目前支持的關鍵字如下。
Keyword |
Sample |
JPQL snippet |
IsNotNull |
findByAgeNotNull |
… where x.age not null |
Like |
findByNameLike |
… where x.name like ?1 |
NotLike |
findByNameNotLike |
… where x.name not like ?1 |
StartingWith |
findByNameStartingWith |
… where x.name like ?1(parameter bound with appended %) |
EndingWith |
findByNameEndingWith |
… where x.name like ?1(parameter bound with prepended %) |
Containing |
findByNameContaining |
… where x.name like ?1(parameter bound wrapped in %) |
OrderBy |
findByAgeOrderByName |
… where x.age = ?1 order by x.name desc |
Not |
findByNameNot |
… where x.name <> ?1 |
In |
findByAgeIn |
… where x.age in ?1 |
NotIn |
findByAgeNotIn |
… where x.age not in ?1 |
True |
findByActiveTrue |
… where x.avtive = true |
Flase |
findByActiveFalse |
… where x.active = false |
And |
findByNameAndAge |
… where x.name = ?1 and x.age = ?2 |
Or |
findByNameOrAge |
… where x.name = ?1 or x.age = ?2 |
Between |
findBtAgeBetween |
… where x.age between ?1 and ?2 |
LessThan |
findByAgeLessThan |
… where x.age < ?1 |
GreaterThan |
findByAgeGreaterThan |
… where x.age > ?1 |
After/Before |
… |
… |
IsNull |
findByAgeIsNull |
… where x.age is null |
2.JpaRepository相關查詢功能
a.Spring DataJPA框架在進行方法名解析時,會先把方法名多餘的前綴截取掉,比如find、findBy、read、readBy、get、getBy,然後對剩下部分進行解析。
b.假如創建如下的查詢:findByUserDepUuid(),框架在解析該方法時,首先剔除findBy,然後對剩下的屬性進行解析,假設查詢實體為Doc。
1:先判斷userDepUuid (根據POJO規范,首字母變為小寫)是否為查詢實體的一個屬性,如果是,則表示根據該屬性進行查詢;如果沒有該屬性,繼續第二步;
2:從右往左截取第一個大寫字母開頭的字符串此處為Uuid),然後檢查剩下的字符串是否為查詢實體的一個屬性,如果是,則表示根據該屬性進行查詢;如果沒有該屬性,則重復第二步,繼續從右往左截取;最後假設user為查詢實體的一個屬性;
3:接著處理剩下部分(DepUuid),先判斷user所對應的類型是否有depUuid屬性,如果有,則表示該方法最終是根據“Doc.user.depUuid” 的取值進行查詢;否則繼續按照步驟2的規則從右往左截取,最終表示根據“Doc.user.dep.uuid” 的值進行查詢。
4:可能會存在一種特殊情況,比如Doc包含一個user的屬性,也有一個userDep 屬性,此時會存在混淆。可以明確在屬性之間加上”_”以顯式表達意圖,比如”findByUser_DepUuid()”或者”findByUserDep_uuid()”
c.特殊的參數: 還可以直接在方法的參數上加入分頁或排序的參數,比如:
Page<UserModel>findByName(String name, Pageable pageable); List<UserModel>findByName(String name, Sort sort);
d.也可以使用JPA的NamedQueries,方法如下:
1:在實體類上使用@NamedQuery:
@NamedQuery(name ="UserModel.findByAge",query = "select o from UserModel o where o.age >=?1")
2:在自己實現的DAO的Repository接口裡面定義一個同名的方法,示例如下:
publicList<UserModel> findByAge(int age);
3:然後就可以使用瞭,Spring會先找是否有同名的NamedQuery,如果有,那麼就不會按照接口定義的方法來解析。
e.還可以使用@Query來指定本地查詢,隻要設置nativeQuery為true,比如:
@Query(value="select* from tbl_user where name like %?1" ,nativeQuery=true) publicList<UserModel> findByUuidOrAge(String name);
註意:當前版本的本地查詢不支持翻頁和動態的排序
f.使用命名化參數,使用@Param即可,比如:
@Query(value="selecto from UserModel o where o.name like %:nn") publicList<UserModel> findByUuidOrAge(@Param("nn") String name);
g.同樣支持更新類的Query語句,添加@Modifying即可,比如:
@Modifying @Query(value="updateUserModel o set o.name=:newName where o.name like %:nn") public intfindByUuidOrAge(@Param("nn") String name,@Param("newName")String newName);
註意:
1:方法的返回值應該是int,表示更新語句所影響的行數
2:在調用的地方必須加事務,沒有事務不能正常執行
f.創建查詢的順序
Spring Data JPA在為接口創建代理對象時,如果發現同時存在多種上述情況可用,它該優先采用哪種策略呢?
<jpa:repositories>提供瞭query-lookup-strategy 屬性,用以指定查找的順序。
它有如下三個取值:
1:create-if-not-found:如果方法通過@Query指定瞭查詢語句,則使用該語句實現查詢;如果沒有,則查找是否定義瞭符合條件的命名查詢,如果找到,則使用該命名查詢;如果兩者都沒有找到,則通過解析方法名字來創建查詢。這是querylookup-strategy 屬性的默認值
2:create:通過解析方法名字來創建查詢。即使有符合的命名查詢,或者方法通過
@Query指定的查詢語句,都將會被忽略
3:use-declared-query:如果方法通過@Query指定瞭查詢語句,則使用該語句實現查詢;如果沒有,則查找是否定義瞭符合條件的命名查詢,如果找到,則使用該命名查詢;如果兩者都沒有找到,則拋出異常
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- 基於JPA的Repository使用詳解
- 教你快速學會JPA中所有findBy語法規則
- 使用JPA自定義SQL查詢結果
- Spring Data Exists查詢最佳方法編寫示例
- SpringBoot中JPA實現Sort排序的三種方式小結