引言
什么是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)等。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。