使用任意 AND 子句的動態 Spring Data JPA 儲存庫查詢
1. 概述
在使用 Spring Data 開發應用程式時,我們經常需要根據選擇標準建立動態查詢以從資料庫中取得資料。
本教學探討了在 Spring Data JPA 儲存庫中建立動態查詢的三種方法:透過Example
查詢、透過Specification,
查詢和透過 Querydsl 查詢。
2. 場景設定
在我們的演示中,我們將建立兩個實體School
和Student
。這些班級之間的關聯是一對多的,其中一所School
可以有許多Student
:
@Entity
@Table
public class School {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column
private Long id;
@Column
private String name;
@Column
private String borough;
@OneToMany(mappedBy = "school")
private List<Student> studentList;
// constructor, getters and setters
}
@Entity
@Table
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column
private Long id;
@Column
private String name;
@Column
private Integer age;
@ManyToOne
private School school;
// constructor, getters and setters
}
除了實體類別之外,我們也為Student
實體定義一個 Spring Data 儲存庫:
public interface StudentRepository extends JpaRepository<Student, Long> {
}
最後,我們將在School
表中添加一些範例資料:
ID | 姓名 | 行政區 |
---|---|---|
1 | 西倫敦大學 | 伊靈 |
2 | 金斯頓大學 | 泰晤士河畔金斯頓 |
讓我們對Student
表也做同樣的事情:
ID | 姓名 | 年齡 | 學校ID |
---|---|---|---|
1 | 艾蜜莉史密斯 | 20 | 2 |
2 | 詹姆斯史密斯 | 20 | 1 |
3 | 瑪麗亞·約翰遜 | 22 | 1 |
4 | 麥可布朗 | 21 | 1 |
5 | 索菲亞·史密斯 | 22 | 1 |
在後續部分中,我們將透過以下標準使用不同的方法來尋找記錄:
-
Student
的name
以Smith
結尾,並且 -
Student
age
為20
,且 - 該
Student
的學校位於Ealing
區
3. Example
查詢
Spring Data 提供了一種使用範例查詢實體的簡單方法。這個想法很簡單:我們建立一個範例實體並將我們要尋找的搜尋條件放入其中。然後,我們使用這個範例來尋找與其相符的實體。
要採用這一點,儲存庫必須實作介面QueryByExampleExecutor
。在我們的例子中,該介面已經在JpaRepository
中擴展,我們通常在儲存庫中擴展該介面。因此,沒有必要明確地實現它。
現在,讓我們建立一個Student
範例以包含我們要過濾的三個選擇標準:
School schoolExample = new School();
schoolExample.setBorough("Ealing");
Student studentExample = new Student();
studentExample.setAge(20);
studentExample.setName("Smith");
studentExample.setSchool(schoolExample);
Example example = Example.of(studentExample);
設定範例後,我們呼叫儲存庫findAll(…)
方法來取得結果:
List<Student> studentList = studentRepository.findAll(example);
但是,上面的範例僅支援精確匹配。如果我們想取得名字以「 Smith
」結尾的學生,我們需要客製化配對策略。 Query by example 提供了ExampleMatcher
類別來執行此操作。我們需要做的就是在name
欄位上建立一個ExampleMatcher
並將其套用到Example
實例:
ExampleMatcher customExampleMatcher = ExampleMatcher.matching()
.withMatcher("name", ExampleMatcher.GenericPropertyMatchers.endsWith().ignoreCase());
Example<Student> example = Example.of(studentExample, customExampleMatcher);
我們在這裡使用的ExampleMatcher
是不言自明的。它使名稱欄位在匹配時不區分大小寫,並確保值以我們範例中指定的名稱結尾。
透過Example
查詢易於理解和實現。但是,它不支援更複雜的查詢,例如大於或小於欄位的條件。
4.依Specification
查詢
Spring Data JPA 中的依Specification
查詢允許使用Specification
介面基於一組條件建立動態查詢。
與傳統方法(例如衍生查詢方法或使用@Query
的自訂查詢)相比,這種方法更加靈活。它對於複雜的查詢要求或需要在運行時動態調整查詢時非常有用。
與範例查詢類似,我們的儲存庫方法必須擴展一個介面才能啟用此功能。這次,我們需要擴充JpaSpecificationExecutor
:
public interface StudentRepository extends JpaRepository<Student, Long>, JpaSpecificationExecutor<Student> {
}
接下來,我們必須為每個篩選條件定義三個方法。 Specification
並不限制我們對一種過濾條件使用一種方法。這主要是為了清晰起見並使其更具可讀性:
public class StudentSpecification {
public static Specification<Student> nameEndsWithIgnoreCase(String name) {
return (root, query, criteriaBuilder) ->
criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), "%" + name.toLowerCase());
}
public static Specification<Student> isAge(int age) {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("age"), age);
}
public static Specification<Student> isSchoolBorough(String borough) {
return (root, query, criteriaBuilder) -> {
Join<Student, School> scchoolJoin = root.join("school");
return criteriaBuilder.equal(scchoolJoin.get("borough"), borough);
};
}
}
從上面的方法我們知道,我們使用CriteriaBuilder
來建構濾波條件。 CriteriaBuilder
幫助我們以程式設計方式在 JPA 中建立動態查詢,並為我們提供類似於編寫 SQL 查詢的靈活性。它允許我們使用equal(…)
和like(…)
等方法創建謂詞來定義條件。
在更複雜的操作的情況下,例如將附加表與基底表連接時,我們將使用Root.join(…)
。根充當 FROM 子句的錨點,提供對實體屬性和關係的存取。
現在,讓我們呼叫儲存庫方法來取得Specification
過濾的結果:
Specification<Student> studentSpec = Specification
.where(StudentSpecification.nameEndsWithIgnoreCase("smith"))
.and(StudentSpecification.isAge(20))
.and(StudentSpecification.isSchoolBorough("Ealing"));
List<Student> studentList = studentRepository.findAll(studentSpec);
5.透過QueryDsl查詢
與Example
相比, Specification
強大,能夠處理更複雜的查詢。然而,當我們處理包含許多選擇條件的複雜查詢時, Specification
介面可能會變得冗長且難以閱讀。
這就是 QueryDSL 試圖用更直觀的解決方案來解決Specification
的局限性的地方。它是一個類型安全的框架,用於以直覺、可讀和強類型的方式建立動態查詢。
為了使用 QueryDSL,我們需要添加一些依賴項。讓我們將以下Querydsl JPA和APT 支援依賴項新增到pom.xml
中:
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>5.1.0</version>
<classifier>jakarta</classifier>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>5.1.0</version>
<classifier>jakarta</classifier>
<scope>provided</scope>
</dependency>
值得注意的是,JPA 3.0 中 JPA 的套件名稱從javax.persistence
改為jakarata.persistence
。如果我們使用 3.0 及更高版本,我們必須將jakarta
分類器放入依賴項中。
除了依賴項之外,我們還必須在pom.xml
的插件部分中包含以下註釋處理器:
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources</outputDirectory>
<processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
該處理器在編譯時為我們的實體類別產生元模型類別。將這些設定合併到我們的應用程式中並進行編譯後,我們將看到 Querydsl 在建置資料夾中產生兩種查詢類型QStudent
和QSchool
。
以類似的方式,我們這次必須包含QuerydslPredicateExecutor
,以使我們的儲存庫能夠透過這些查詢類型取得結果:
public interface StudentRepository extends JpaRepository<Student, Long>, QuerydslPredicateExecutor<Student>{
}
接下來,我們將根據這些查詢類型建立一個動態查詢,並在我們的StudentRepository
中使用它進行查詢。這些查詢類型已經包含了對應實體類別的所有屬性。因此,我們可以直接引用建構謂詞時所需的欄位:
QStudent qStudent = QStudent.student;
BooleanExpression predicate = qStudent.name.endsWithIgnoreCase("smith")
.and(qStudent.age.eq(20))
.and(qStudent.school.borough.eq("Ealing"));
List studentList = (List) studentRepository.findAll(predicate);
如上面的程式碼所示,使用查詢類型定義查詢條件非常簡單直覺。
儘管設定所需的依賴關係很複雜,這是一項一次性任務,但它提供了與Specification
相同的流暢方式,直觀且易於閱讀。
而且,我們不需要按照Specification
類別中的要求手動明確定義過濾條件。
六、結論
在本文中,我們探索了在 Spring Data JPA 中建立動態查詢的不同方法。
-
Example
查詢最適合簡單、精確比對的查詢。 - 如果我們需要更多類似 SQL 的表達式和比較,則按
Specification
查詢非常適合中等複雜的查詢。 - QueryDSL 查詢最適合高度複雜的查詢,因為它使用查詢類型定義條件很簡單。
與往常一樣,我們範例的完整原始程式碼可以在 GitHub 上找到。