Spring Data JPA 中具有 SpEL 支援的 @Query 定義
1. 概述
SpEL 代表 Spring 表達式語言,是一個功能強大的工具,可以顯著增強我們與 Spring 的交互,並提供對配置、屬性設定和查詢操作的額外抽象。
在本教程中,我們將學習如何使用此工具使自訂查詢更加動態,並在儲存庫層中隱藏特定於資料庫的操作。我們將使用@Query
註釋,它允許我們使用 JPQL 或本機 SQL 來自訂與資料庫的交互作用。
2. 訪問參數
我們先檢查如何使用 SpEL 來處理方法參數。
2.1.透過索引訪問
透過索引存取參數並不是最佳選擇,因為它可能會為程式碼帶來難以偵錯的問題。特別是當參數具有相同類型時。
同時,它為我們提供了更大的靈活性,尤其是在參數名稱經常變化的開發階段。 IDE 可能無法正確處理程式碼和查詢中的更新。
JDBC 為我們提供了?
我們可以使用佔位符來標識參數在查詢中的位置。 Spring 支援這種約定並允許編寫以下內容:
@Modifying
@Transactional
@Query(value = "INSERT INTO articles (id, title, content, language) "
+ "VALUES (?1, ?2, ?3, ?4)",
nativeQuery = true)
void saveWithPositionalArguments(Long id, String title, String content, String language);
到目前為止,沒有什麼有趣的事情發生。我們使用的方法與先前在 JDBC 應用程式中使用的方法相同。請注意,任何在資料庫中進行更改的查詢都需要@Modifying
和@Transactional
註釋,INSERT 就是其中之一。 INSERT 的所有範例都將使用本機查詢,因為 JPQL 不支援它們。
我們可以使用 SpEL 來重寫上面的查詢:
@Modifying
@Transactional
@Query(value = "INSERT INTO articles (id, title, content, language) "
+ "VALUES (?#{[0]}, ?#{[1]}, ?#{[2]}, ?#{[3]})",
nativeQuery = true)
void saveWithPositionalSpELArguments(long id, String title, String content, String language);
結果類似,但看起來比前一個更混亂。然而,由於它是 SpEL,它為我們提供了所有豐富的功能。例如,我們可以在查詢中使用條件邏輯:
@Modifying
@Transactional
@Query(value = "INSERT INTO articles (id, title, content, language) "
+ "VALUES (?#{[0]}, ?#{[1]}, ?#{[2] ?: 'Empty Article'}, ?#{[3]})",
nativeQuery = true)
void saveWithPositionalSpELArgumentsWithEmptyCheck(long id, String title, String content, String isoCode);
我們在此查詢中使用Elvis operator
來檢查是否提供了內容。儘管我們可以在查詢中編寫更複雜的邏輯,但應謹慎使用它,因為它可能會帶來偵錯和驗證程式碼的問題。
2.2.透過名稱訪問
我們存取參數的另一種方法是使用命名佔位符,它通常與參數名稱匹配,但這不是嚴格要求。這是 JDBC 的另一個約定;命名參數以:name
佔位符標記。我們可以直接使用它:
@Modifying
@Transactional
@Query(value = "INSERT INTO articles (id, title, content, language) "
+ "VALUES (:id, :title, :content, :language)",
nativeQuery = true)
void saveWithNamedArguments(@Param("id") long id, @Param("title") String title,
@Param("content") String content, @Param("isoCode") String language);
唯一需要做的額外事情是確保 Spring 知道參數的名稱。我們可以以更隱式的方式執行此操作並使用-parameters
標誌編譯程式碼,也可以使用@Param
註解明確執行此操作。
顯式方式總是更好,因為它提供了對名稱的更多控制,而且我們不會因為不正確的編譯而遇到問題。
不過,讓我們使用 SpEL 來重寫相同的查詢:
@Modifying
@Transactional
@Query(value = "INSERT INTO articles (id, title, content, language) "
+ "VALUES (:#{#id}, :#{#title}, :#{#content}, :#{#language})",
nativeQuery = true)
void saveWithNamedSpELArguments(@Param("id") long id, @Param("title") String title,
@Param("content") String content, @Param("language") String language);
這裡,我們有標準的 SpEL 語法,但此外,我們需要使用#
來區分參數名稱和應用程式 bean。如果我們省略它,Spring 將嘗試在上下文中尋找名稱為id
、 title
、 content
和language
的 bean 。
總的來說,這個版本與沒有 SpEL 的簡單方法非常相似。然而,如上一節所討論的,SpEL 提供了更多的能力和功能。例如,我們可以呼叫傳遞的物件上可用的函數:
@Modifying
@Transactional
@Query(value = "INSERT INTO articles (id, title, content, language) "
+ "VALUES (:#{#id}, :#{#title}, :#{#content}, :#{#language.toLowerCase()})",
nativeQuery = true)
void saveWithNamedSpELArgumentsAndLowerCaseLanguage(@Param("id") long id, @Param("title") String title,
@Param("content") String content, @Param("language") String language);
我們可以在String
物件上使用toLowerCase()
方法。我們可以執行條件邏輯、方法呼叫、 Strings
連接等。同時, @Query
中的邏輯過多可能會掩蓋它,並且很容易將業務邏輯洩漏到基礎設施程式碼中。
2.3.訪問對象的字段
雖然以前的方法或多或少反映了 JDBC 和準備好的查詢的功能,但這種方法允許我們以更面向對象的方式使用本機查詢。正如我們之前所看到的,我們可以使用簡單的邏輯並呼叫 SpEL 中物件的方法。此外,我們還可以存取物件的欄位:
@Modifying
@Transactional
@Query(value = "INSERT INTO articles (id, title, content, language) "
+ "VALUES (:#{#article.id}, :#{#article.title}, :#{#article.content}, :#{#article.language})",
nativeQuery = true)
void saveWithSingleObjectSpELArgument(@Param("article") Article article);
我們可以使用物件的公共 API 來取得其內部結構。這是一項非常有用的技術,因為它允許我們保持儲存庫的簽名整潔並且不會暴露太多。它甚至允許我們存取嵌套物件。假設我們有一個文章包裝器:
public class ArticleWrapper {
private final Article article;
public ArticleWrapper(Article article) {
this.article = article;
}
public Article getArticle() {
return article;
}
}
我們可以在我們的範例中使用它:
@Modifying
@Transactional
@Query(value = "INSERT INTO articles (id, title, content, language) "
+ "VALUES (:#{#wrapper.article.id}, :#{#wrapper.article.title}, "
+ ":#{#wrapper.article.content}, :#{#wrapper.article.language})",
nativeQuery = true)
void saveWithSingleWrappedObjectSpELArgument(@Param("wrapper") ArticleWrapper articleWrapper);
因此,我們可以將參數視為 SpEL 中的 Java 對象,並使用任何可用的欄位或方法。我們也可以向該查詢新增邏輯和方法呼叫。
此外,我們可以將此技術與Pageable
結合使用,從物件中獲取信息,例如偏移量或頁面大小,並將其添加到我們的本機查詢中。雖然Sort
也是一個對象,但它的結構更複雜,使用起來也會更困難。
3. 引用實體
減少重複程式碼是一個很好的做法。然而,自訂查詢可能會使其具有挑戰性。即使我們有類似的邏輯來提取到基本儲存庫,表的名稱也不同,因此很難重複使用它們。
SpEL 為實體名稱提供佔位符,該佔位符是從儲存庫參數化推斷出來的。讓我們建立一個這樣的基礎儲存庫:
@NoRepositoryBean
public interface BaseNewsApplicationRepository<T, ID> extends JpaRepository<T, ID> {
@Query(value = "select e from #{#entityName} e")
List<Article> findAllEntitiesUsingEntityPlaceholder();
@Query(value = "SELECT * FROM #{#entityName}", nativeQuery = true)
List<Article> findAllEntitiesUsingEntityPlaceholderWithNativeQuery();
}
我們必須使用一些附加註釋才能使其正常工作。第一個是@NoRepositoryBean.
我們需要這個來從實例化中排除這個基礎儲存庫。由於它沒有特定的參數化,因此嘗試建立此類儲存庫將使上下文失敗。因此,我們需要排除它。
使用 JPQL 的查詢非常簡單,將使用給定儲存庫的實體名稱:
@Query(value = "select e from #{#entityName} e")
List<Article> findAllEntitiesUsingEntityPlaceholder();
然而,本機查詢的情況並非那麼簡單。無需任何其他更改和配置,它將嘗試使用實體名稱(在我們的範例中為Article
)來查找表:
@Query(value = "SELECT * FROM #{#entityName}", nativeQuery = true)
List<Article> findAllEntitiesUsingEntityPlaceholderWithNativeQuery();
但是,我們的資料庫中沒有這樣的表。在實體定義中,我們明確指出了表格的名稱:
@Entity
@Table(name = "articles")
public class Article {
// ...
}
為了解決這個問題,我們需要向我們的表提供名稱匹配的實體:
@Entity(name = "articles")
@Table(name = "articles")
public class Article {
// ...
}
在這種情況下,JPQL 和本機查詢都將推斷出正確的實體名稱,並且我們將能夠在應用程式中的所有實體中重複使用相同的基本查詢。
4. 新增 SpEL 上下文
如所指出的,在引用參數或占位符時,我們必須在它們的名稱之前提供一個額外的#
。這樣做是為了區分 bean 名稱和參數名稱。
但是,我們不能直接在查詢中使用 Spring 上下文中的 bean。 IDE 通常會從上下文中提供有關 Bean 的提示,但上下文會失敗。發生這種情況是因為@Value
和類似的註解以及@Query
處理方式不同。我們可以從前者的上下文中引用 beans,但不能從後者的上下文中引用 beans。
同時,我們可以使用EvaluationContextExtension
在SpEL上下文中註冊bean,這樣我們就可以在**@Query.**
讓我們想像一下以下情況 - 我們希望從資料庫中找到所有文章,但根據用戶的區域設定對它們進行過濾:
@Query(value = "SELECT * FROM articles WHERE language = :#{locale.language}", nativeQuery = true)
List<Article> findAllArticlesUsingLocaleWithNativeQuery();
此查詢將失敗,因為預設情況下我們無法存取區域設定。我們需要提供自訂的EvaluationContextExtension
來保存有關使用者區域設定的資訊:
@Component
public class LocaleContextHolderExtension implements EvaluationContextExtension {
@Override
public String getExtensionId() {
return "locale";
}
@Override
public Locale getRootObject() {
return LocaleContextHolder.getLocale();
}
}
我們可以使用LocaleContextHolder
來存取應用程式中任何位置的當前區域設定。唯一需要注意的是,它與用戶的請求相關聯,並且在此範圍之外無法存取。我們需要提供根物件和名稱。 (可選)我們也可以新增屬性和函數,但在此範例中我們將僅使用根物件。
在能夠在@Query
中使用locale
之前,我們需要採取的另一個步驟是註冊語言環境攔截器:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("locale");
registry.addInterceptor(localeChangeInterceptor);
}
}
在這裡,我們可以添加有關我們將追蹤的參數的信息,因此每當請求包含區域設定參數時,上下文中的區域設定都會更新。可以透過在請求中提供區域設定來檢查邏輯:
@ParameterizedTest
@CsvSource({"eng,2","fr,2", "esp,2", "deu, 2","jp,0"})
void whenAskForNewsGetAllNewsInSpecificLanguageBasedOnLocale(String language, int expectedResultSize) {
webTestClient.get().uri("/articles?locale=" + language)
.exchange()
.expectStatus().isOk()
.expectBodyList(Article.class)
.hasSize(expectedResultSize);
}
EvaluationContextExtension
可用於顯著增強 SpEL 的功能,尤其是在使用 @Query 註解時。使用它的方法範圍從安全性和角色限製到功能標記和模式之間的交互作用。
5. 結論
SpEL 是一個強大的工具,就像所有強大的工具一樣,人們傾向於過度使用它們並試圖僅使用它來解決所有問題。最好合理地使用複雜的表達式,並且僅在必要的情況下使用。
儘管 IDE 提供 SpEL 支援和突出顯示,但複雜的邏輯可能隱藏難以偵錯和驗證的錯誤。因此,請謹慎使用 SpEL,並避免使用可能更好地用 Java 表達而不是隱藏在 SpEL 中的「智慧代碼」。
與往常一樣,本教程中使用的所有程式碼都可以在 GitHub 上找到。