Specifications 構建動態查詢

吴季分發表於2024-11-21

引言

什麼是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]

篩選條件的謂詞

比較類謂詞等於 =不等於 !=大於 >小於 <大於等於 >=小於等於 <=
邏輯類謂詞andornot
範圍類謂詞between...andnot between...andlikenot likeinnot in
空值類謂詞is nullnot 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...)

準備資料庫的資料

image.png

image.png

三. 實踐使用

針對測試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對比

表列 ASpecificationJPA 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

相關文章