引言
什麼是Specifications?
Specifications是JPA(Java Persistence API)提供的一種強大且靈活的查詢構建方式。它允許我們透過組合各種條件(如相等、不等、包含、範圍等),動態地構建複雜的查詢語句,而無需編寫冗長的SQL(Structured Query Language)或JPQL(Java Persistence Query Language)。
Specifications的作用
動態查詢: 可以根據不同的查詢條件動態構建查詢語句,提高系統的靈活性。
複雜查詢: 支援各種複雜的查詢組合,包括AND、OR、NOT等邏輯運算。
型別安全: 透過型別檢查,避免SQL隱碼攻擊等安全問題。
可讀性: 使用Lambda表示式構建查詢,程式碼更加簡潔易懂。
一. 常見資料庫謂語
SELECT 列名
FROM 表名
WHERE 篩選條件
GROUP BY 分組列
HAVING 分組後的篩選條件
ORDER BY 排序[ ASC | DESC]
篩選條件的謂詞
比較類謂詞 | 等於 = | 不等於 != | 大於 > | 小於 < | 大於等於 >= | 小於等於 <= |
邏輯類謂詞 | and | or | not | |||
範圍類謂詞 | between...and | not between...and | like | not like | in | not in |
空值類謂詞 | is null | not is null | ||||
日期類謂詞 | DATE() | MONTH() | ||||
正則表達類謂詞 | REGEXP 'pattern' |
二. 準備條件
準備實體
Student實體和Clazz實體 (多對一的關係)
@Entity
@Data
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Short age;
@ManyToOne
private Clazz clazz;
}
@Entity
@Data
public class Clazz {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "clazz")
private List<Student> students;
private String teacherName;
}
準備能訪問資料庫的倉庫層
@Repository
public interface ClazzRepository extends JpaRepository<Clazz, Long>, JpaSpecificationExecutor<Clazz> {
}
@Repository
public interface StudentRepository extends JpaRepository<Student, Long>, JpaSpecificationExecutor<Student> {
}
準備測試demo的例子
1 模糊查詢班級的名稱,查出班級下的學生 (兩表需要連線,謂語like)
2 模糊查詢學生的名字 (單表,謂語like)
3 精準查詢學生年齡是X的學生 (單表,謂語 = )
4 查出班級的老師不是null的班級 (單表,謂語not is null)
5 查詢擁有學生年齡在A到B之間的班級 (兩表需要連線,謂語between... and...)
準備資料庫的資料
三. 實踐使用
針對測試demo的例子問題,我們對其進行實現
1 模糊查詢班級的名稱,查出班級下的學生 (兩表需要連線,謂語like)
2 模糊查詢學生的名字 (單表,謂語like)
3 精準查詢學生年齡是X的學生 (單表,謂語 = )
public class StudentSpecifications {
public static Specification<Student> hasClazzName(String name) {
return (root, query, criteriaBuilder) -> criteriaBuilder.like(root.join("clazz", JoinType.LEFT)
.<String>get("name"), "%" + name + "%");
}
public static Specification<Student> hasName(String name) {
return (root, query, criteriaBuilder) -> criteriaBuilder.like(root.<String>get("name"), "%" + name + "%");
}
public static Specification<Student> hasAge(Short age) {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.<Short>get("age"), age);
}
}
進行查詢測試:
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class StudentSpecificationsTest {
@Autowired
private StudentRepository studentRepository;
@Test
void hasClazzName() {
Specification<Student> spec = Specification.where(StudentSpecifications.hasClazzName("件"));
List<Student> studentList = studentRepository.findAll(spec);
studentList.stream().forEach(student ->
System.out.println(student.getName()) // 輸出: 小明 小李
);
}
@Test
void hasAge() {
Specification<Student> spec = Specification.where(StudentSpecifications.hasAge((short) 18));
List<Student> studentList = studentRepository.findAll(spec);
studentList.stream().forEach(student ->
System.out.println(student.getName()) // 輸出: 小明
);
}
@Test
void hasName() {
Specification<Student> spec = Specification.where(StudentSpecifications.hasName("小"));
List<Student> studentList = studentRepository.findAll(spec);
studentList.stream().forEach(student ->
System.out.println(student.getName()) // 輸出: 小明 小李
);
}
}
4 查出班級的老師不是null的班級 (單表,謂語not is null)
5 查詢擁有學生年齡在A到B之間的班級 (兩表需要連線,謂語between... and...)
public class ClazzSpecifications {
public static Specification<Clazz> hasTeacher() {
return ((root, query, criteriaBuilder) -> criteriaBuilder.isNotNull(root.<String>get("teacherName")));
}
public static Specification<Clazz> getByStudent_age(Short a, Short b) {
return (root, query, criteriaBuilder) -> criteriaBuilder.between(root.join("students", JoinType.LEFT)
.<Short>get("age"), a, b);
}
}
進行查詢測試:
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class ClazzSpecificationsTest {
@Autowired
private ClazzRepository clazzRepository;
@Test
void hasTeacher() {
Specification<Clazz> spec = Specification.where(ClazzSpecifications.hasTeacher());
List<Clazz> clazzList= this.clazzRepository.findAll(spec);
clazzList.stream().forEach(clazz -> System.out.println("班級:" + clazz.getName()
+ " 老師:" + clazz.getTeacherName())); // 輸出 班級:軟體1班 老師:周老師
}
@Test
void getByStudent_age() {
Specification<Clazz> spec = Specification.where(ClazzSpecifications.getByStudent_age((short) 20, (short) 25));
List<Clazz> clazzList= this.clazzRepository.findAll(spec);
clazzList.stream().forEach(clazz -> System.out.println("班級:" + clazz.getName())); // 輸出 班級:網路1班
}
}
四 擴充套件JPA Criteria API
JPA Criteria API: 也是用與動態地構建複雜的查詢語句。
JPA Criteria API的基本使用
精準查詢學生年齡是X的學生 (單表)
public static List<Student> getUsersByAge(EntityManager entityManager, int age) {
// 建立CriteriaBuilder物件
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
// 建立criteriaQuery物件
CriteriaQuery<Student> criteriaQuery = criteriaBuilder.createQuery(Student.class);
// 定義查詢根
Root<Student> root = criteriaQuery.from(Student.class);
// 使用select()選擇要返回的實體類,並使用where方法定義查詢條件。
criteriaQuery.select(root).where(criteriaBuilder.equal(root.get("age"), age));
// 使用createQuery()執行查詢 getResultList() 獲取結果
return entityManager.createQuery(criteriaQuery).getResultList();
}
執行測試:
@Test
void getUsersByAge() {
List<Student> studentList = StudentSpecifications.getUsersByAge(this.entityManager, 21);
studentList.stream().forEach(student ->
System.out.println(student.getName()) // 輸出: 張三
);
}
模糊查詢班級的名稱,查出班級下的學生 (兩表需要連線)
public static List<Student> getStudentByClazzName (EntityManager entityManager, String name) {
// 建立CriteriaBuilder物件
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
// 建立CriteriaQuery物件
CriteriaQuery<Student> criteriaQuery = criteriaBuilder.createQuery(Student.class);
// 定義查詢根
Root<Student> rootA = criteriaQuery.from(Student.class);
// 新增連線表
Join<Student, Clazz> joinB = rootA.join("clazz");
// 新增where子句
Predicate predicate = criteriaBuilder.like(joinB.<String>get("name"), "%" + name + "%");
criteriaQuery.where(predicate);
// 執行查詢
return entityManager.createQuery(criteriaQuery).getResultList();
}
執行測試:
@Test
void getStudentByClazzName() {
List<Student> studentList = StudentSpecifications.getStudentByClazzName(this.entityManager, "網路");
studentList.stream().forEach(student ->
System.out.println(student.getName()) // 輸出: 張三
);
}
五 Specification和JPA Criteria API對比
表列 A | Specification | JPA Criteria API |
---|---|---|
來源 | Spring Data JPA中引入的一個介面 | 是JPA 2.0標準的一部分 |
使用場景 | 更適用於Spring Data JPA環境中,需要動態構建查詢條件的場景 | 更適用於需要直接操作JPA實體和查詢構建器的場景 |
整合與依賴 | 引入Spring Data JPA的依賴 | 無需額外的依賴 |
總結
1 Specification是介面,需要實現toPredicate方法
2 Predicate toPredicate(Root<T> root,
@Nullable CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder);
root:代表查詢的根實體
query:是構建查詢的頂級介面。它包含了查詢的根(Root)、選擇(Selection)、分組(Grouping)、排序(Ordering)和限制(Restriction,即Predicate)等所有資訊。
criteriaBuilder:是用於構建Criteria查詢的工廠類。提供了多種方法來建立條件表示式,如等於(equal)、不等於(notEqual)、大於(greaterThan)、小於(lessThan)、在範圍內(between)、為空(isNull)、不為空(isNotNull)、以及邏輯運算(and、or、not)等。
欠缺:
應該建立一個倉庫,把這篇文章涉及到的程式碼傳到github倉庫中,enenen... 這次換電腦了程式碼沒有備份,下次注意!
參考文獻
Specifications
Advanced Spring Data JPA - Specifications and Querydsl