Hibernate 中的 IN 子句參數填充
1. 概述
在建立持久層時,優化資料庫查詢效能是重要要求。
資料庫用來提高查詢效能的一項技術是 SQL 語句緩存,它重複使用先前準備的 SQL 語句,以避免在資料庫引擎中重複產生相同執行計劃的開銷。
然而,在處理 IN 子句時,語句快取會遇到挑戰,因為它們通常具有不同數量的參數。
在本教程中,我們將探討 Hibernate 的參數填充功能如何解決此問題並提高帶有 IN 子句的查詢的語句快取的有效性。
2. 應用程式設定
在我們探索 Hibernate 中參數填充的概念之前,讓我們先設定一個簡單的應用程序,我們將在本教程中使用該應用程式。
2.1.依賴關係
讓我們先將Hibernate 依賴項新增到專案的pom.xml檔案中:
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.5.2.Final</version>
</dependency>
這種依賴關係為我們提供了核心 Hibernate ORM 功能,包括我們在本教程中討論的參數填充功能。
2.2.定義實體類別
現在,讓我們定義我們的實體類別:
@Entity
class Pokemon {
@Id
private UUID id;
private String name;
// standard setters and getters
}
Pokemon類別是我們教程中的中心實體,在接下來的部分中,我們將使用它來學習如何使用參數填充來加速涉及 IN 子句的查詢的資料庫 SQL 查詢執行速度。
3. SQL語句緩存
SQL語句快取是一種用於最佳化資料庫查詢效能的技術。當我們的資料庫收到 SQL 查詢時,它會準備一個執行計劃並執行它以檢索結果。此過程可能非常耗時,尤其是對於複雜的查詢。
為了避免重複這種開銷,資料庫引擎根據準備好的語句會快取查詢執行計劃,並在具有不同參數值的後續執行中重複使用它們。
讓我們考慮一個透過name屬性搜尋Pokemon範例:
String[] names = { "Pikachu", "Charizard", "Bulbasaur" };
String query = "SELECT p FROM Pokemon p WHERE p.name = :name";
for (String name : names) {
Pokemon pokemon = entityManager.createQuery(query, Pokemon.class)
.setParameter("name", name)
.getSingleResult();
assertThat(pokemon)
.isNotNull()
.hasNoNullFieldsOrProperties();
}
在我們的範例中,SQL 語句SELECT p FROM Pokemon p WHERE p.name = :name只準備一次,並在循環的每次迭代中重複使用。
命名參數:name在執行期間被替換為names陣列中儲存的實際參數值。這種快取機制消除了為相同 SQL 查詢重複準備執行計劃的開銷。
4. 使用 IN 子句快取 SQL 語句
雖然 SQL 語句快取在大多數情況下效果很好,但在處理具有不同數量參數的 IN 子句時效率有點低:
String[][] nameGroups = {
{ "Jigglypuff" },
{ "Snorlax", "Squirtle" },
{ "Pikachu", "Charizard", "Bulbasaur" }};
String query = "SELECT p FROM Pokemon p WHERE p.name IN :names";
for (String[] names : nameGroups) {
List<Pokemon> pokemons = entityManager.createQuery(query, Pokemon.class)
.setParameter("names", Arrays.asList(names))
.getResultList();
assertThat(pokemons)
.isNotEmpty();
}
在我們的範例中,我們有一組Pokemon名稱,用於使用 IN 子句檢索Pokemon實體。但是,每個群組具有不同數量的名稱,導致 IN 子句中的參數數量不同。
在這種情況下,資料庫為每個具有不同數量參數的查詢產生單獨的執行計劃。因此,語句快取變得無效,因為每個查詢都被視為一個新語句。
5. IN 子句的參數填充
為了解決 IN 子句的 SQL 語句快取問題,Hibernate 5.2.18 引入了參數填充功能。即使 IN 子句中的參數數量發生變化,參數填充也允許我們重複使用快取的語句。
我們可以透過將persistence.xml檔案中的hibernate.query.in_clause_parameter_padding屬性設為true來啟用此功能:
<property>
name="hibernate.query.in_clause_parameter_padding"
value="true"
</property>
使用 Spring Data JPA 時,我們可以透過將以下配置新增至application.yaml檔案來啟用參數填入:
spring:
jpa:
properties:
hibernate:
query:
in_clause_parameter_padding: true
啟用參數填入後,Hibernate 會將 IN 子句中的參數數量調整為最接近的 2 次冪,透過重複最後一個參數值來填入清單。
例如,如果我們的 IN 子句包含 3 個參數,Hibernate 會將其填入 4 個參數。這可確保只為具有 3 或 4 個參數的查詢準備一個執行計劃。
同樣,如果 IN 子句中的參數數量在 5 到 8 之間,Hibernate 將在準備好的語句中使用 8 個參數。
為了更好地理解這一點,我們將在應用程式中啟用 SQL 日誌記錄並查看綁定參數:
List<String> names = List.of("Pikachu", "Charizard", "Bulbasaur");
String query = "SELECT p FROM Pokemon p WHERE p.name IN :names";
entityManager.createQuery(query)
.setParameter("names", names);
當我們運行上面的程式碼時,我們將看到以下日誌輸出:
org.hibernate.SQL - select p1_0.id,p1_0.name from pokemon p1_0 where p1_0.name in (?,?,?,?)
org.hibernate.orm.jdbc.bind - binding parameter (1:VARCHAR) <- [Pikachu]
org.hibernate.orm.jdbc.bind - binding parameter (2:VARCHAR) <- [Charizard]
org.hibernate.orm.jdbc.bind - binding parameter (3:VARCHAR) <- [Bulbasaur]
org.hibernate.orm.jdbc.bind - binding parameter (4:VARCHAR) <- [Bulbasaur]
儘管我們提供了三個名稱,但 Hibernate 已將 IN 子句填入四個參數。它重複最後一個值Bulbasaur來填充第四個槽。
此功能有助於減少建立的執行計劃的數量,從而提高使用 IN 子句時的效能和記憶體使用率。
6. 當參數填充失敗時
雖然參數填充是加快資料庫中 SQL 查詢執行速度的一項出色功能,但在某些情況下,它可能無法提供預期的好處,甚至會降低效能。
首先,參數填充對於不快取執行計劃的資料庫沒有用處,例如SQLite、具有BLACKHOLE儲存引擎的MySQL等。在這種情況下,啟用參數填充可能會因為附加參數而引入不必要的開銷。
此外,當 IN 子句中的參數數量非常小或非常大時,啟用參數填充可能沒有幫助。如果參數數量始終很小,則參數填充的好處將可以忽略不計。另一方面,如果參數數量非常大,參數填充會導致快取中記憶體消耗過多,可能會影響效能。
七、結論
在本文中,我們探討了 Hibernate 中參數填充的概念以及它如何解決使用 IN 子句進行 SQL 語句快取的挑戰。
我們了解到,啟用hibernate.query.in_clause_parameter_padding屬性允許 Hibernate 將 IN 子句中的參數數量調整為最接近的 2 的冪,從而有效減少快取語句的數量並重複使用它們以提高效能。
與往常一樣,本文中使用的所有程式碼範例都可以在 GitHub 上找到。