3

引言

什么是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


吴季分
390 声望13 粉丝