Spring Data JPA 在 @Query 中使用投影的方法

西多士發表於2022-07-17

Spring Data JPA 在 @Query 中使用投影的方法

關於投影的基本使用可以參考這篇文章:https://www.baeldung.com/spring-data-jpa-projections。下文沿用了這篇文章中的示例程式碼。

投影的官方文件連結是:https://docs.spring.io/spring-data/jpa/docs/2.6.5/reference/html/#projections (我這裡使用的是 2.6.5 的版本)。

背景鋪墊完畢,接下來開始正文。

最近在寫需求的時候用到了投影來減少資料庫查詢的欄位,結果發現官方文件中挖了個坑= =。官方文件中以及另一篇示例文章中,全程使用了方法名派生的查詢方式,而投影的文件中卻全程沒有提到示例的內容方法名派生的查詢方式下才有效。
那麼,方法名派生的查詢方式好用嗎?對於簡單的只有兩三個欄位的查詢來說,確實方便好用,但條件一多,問題就來了,如果有五六個欄位要過濾,那方法名簡直長的不能看,並且很多查詢預設值都需要通過引數傳進來而不是直接內建到 SQL 中。
在這種時候我更偏好使用自定義查詢的方式,直接面向 SQL 程式設計,比看巨長的方法名要容易的多。

當我在這次需求中把投影和自定義查詢一結合,這坑它就來了...

上面提過,使用投影是為了減少資料庫查詢的欄位。而直接執行示例程式碼的時候也確實看到了這個效果:

測試程式碼

@Test
public void whenUsingOpenProjections_thenViewWithRequiredPropertiesIsReturned() {
        PersonView personView = personRepository.findByLastName("Doe");
}


public interface PersonView {

    String getLastName();

}


@Entity
public class Person {
    @Id
    private Long id;
    private String firstName;
    private String lastName;
}    

執行的 SQL

select person0_.last_name as col_0_0_ from person person0_ where person0_.last_name=?

然後當我換成自定義查詢的方式時,效果就變成了這樣:

測試程式碼

@Query("select p from Person p where p.lastName = ?1")
PersonView findByLastNameByQuery(String lastName);

@Test
public void whenUsingOpenProjections_thenViewWithRequiredPropertiesIsReturned2() {
        PersonView personView = personRepository.findByLastNameByQuery("Doe");
}

執行的SQL

select person0_.id as id1_6_, person0_.first_name as first_na2_6_, person0_.last_name as last_nam3_6_ from person person0_ where person0_.last_name=?

可以看到這裡是查詢了全部的欄位(實在是讓人摸不著頭腦)。

後來有同事提醒說是因為我寫了select p導致的,我就嘗試寫明要查詢的欄位(但還是無法理解為什麼在這種情況下投影直接不生效):
測試程式碼

@Query("select p.lastName from Person p where p.lastName = ?1")
PersonView findByLastNameByQuery(String lastName);

執行的 SQL

select person0_.last_name as col_0_0_ from person person0_ where person0_.last_name=?

從 SQL 上來看,這樣寫已經是實現了我想要的效果,可是實際上真正使用這個程式碼的時候,坑就又來了:
測試程式碼

@Test
public void whenUsingOpenProjections_thenViewWithRequiredPropertiesIsReturned2() {
        PersonView personView = personRepository.findByLastNameByQuery("Doe");
        assertThat(personView.getLastName()).isEqualTo("Doe");
}

加了一行斷言來模擬使用的場景
執行結果

org.opentest4j.AssertionFailedError: 
expected: "Doe"
 but was: null
Expected :"Doe"
Actual   :null

直接黑人問號臉。

分析了一下,執行的 SQL 沒有問題,投影類也沒有問題,那問題就是出在結果集對映的時候了。雖然沒看過 JPA 的程式碼,但是最終肯定是基於 JDBC API 的,而JDBC API是怎麼處理結果集對映的?
翻一翻 ResultSet 類可以看到一共有兩種方法獲取結果:by indexby name,仔細看看執行的 SQL,person0_.last_name as col_0_0_ last_name 自動生成了一個別名叫col_0_0_,而投影類中能獲得的資訊只有欄位名last_name而沒有別名col_0_0_,所以 by name 的路走不通;
那麼by index呢,很明顯也不行,我這裡的示例只有一個欄位,假如有兩個欄位,那麼SQL 中的欄位的順序和投影類中的欄位的順序就無法保證一致,從而就無法根據 index 來獲取想要的對應的結果。

然後就是驗證環節了,假如是因為名字對映不上導致的結果為 null,那我就給你一個能對應的名字:
測試程式碼

@Query("select p.lastName as lastName from Person p where p.lastName = ?1")
PersonView findByLastNameByQuery(String lastName);

@Test
public void whenUsingOpenProjections_thenViewWithRequiredPropertiesIsReturned2() {
        PersonView personView = personRepository.findByLastNameByQuery("Doe");
        assertThat(personView.getLastName()).isEqualTo("Doe");
}

執行的 SQL

select person0_.last_name as col_0_0_ from person person0_ where person0_.last_name=?

雖然執行的 SQL 上還是用了自動生成的別名,但是斷言卻通過了,猜測是 JPA 在解析 Query 的時候儲存了手動宣告的別名資訊。

最後總結一下,如果要在 @Query 中使用投影,必須要主動宣告要查詢的欄位,並且主動寫明欄位的別名才行。

最後的最後,再吐槽一下 JPA,文件中提到投影除了基於介面之外,還可以基於類來實現,然鵝當你想在 @Query 中使用基於類的投影時,?~。

相關文章