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。