Specification:规范、规格
★ Specification查询
它也是Spring Data提供的查询——是对JPA本身 Criteria 动态查询 的包装。
▲ 为何要有动态查询
页面上常常会让用户添加不同的查询条件,程序就需要根据用户输入的条件,动态地组合不同的查询条件。
JPA为动态查询提供了Criteria查询支持。
Spring Data JPA则对Criteria查询进行了封装,封装之后的结果就是Specification查询。
——Specification查询比Jpa的Criteria动态查询更加简单。
如图:
▲ 核心API: JpaSpecificationExecutor
- long count(Specification spec): 返回符合Specification条件的实体的总数。
- List findAll(Specification spec): 返回符合Specification条件的实体。
- Page findAll(Specification spec, Pageable pageable): 返回符合Specification条件的实体,额外传入的Pageable参数用于控制排序和分页。
- List findAll(Specification spec, Sort sort): 返回符合Specification条件的实体,额外传入的Sort参数用于控制排序。
- Optional findOne(Specification spec): 返回符合Specification条件的单个实体,如果符合条件的实体有多个,该方法将会引发异常。
▲ Specification查询的步骤:
(1)让你的DAO接口继承JpaSpecificationExecutor 这个核心API。
(2)构建Specification对象,用于以面向对象的方式来动态地组合查询条件。
——最方便的地方(改进的地方),这一步就是为了解决动态拼接SQL的问题,
而改为使用面向对象的方式来组合查询条件。
▲ 如何创建Specification对象(用于组合多个查询条件)
- Specification参数用于封装多个代表查询条件的Predicate对象。
- Specification接口只定义了一个toPredicate()方法,
该方法返回的Predicate对象就是Specification查询的查询条件,
程序通常使用Lambda表达式来实现toPredicate()方法来定义动态查询条件。
▲ 还涉及如下两个API(本身就是来自于JPA的规范)
Predicate - 代表了单个查询条件,相当于sql语句的where子句中的单个的条件
(比如 age>100,就是一个Predicate )。
也可用于组合多个查询条件。
CriteriaBuilder - 专门用于构建单个Predicate。
代码演示
下面的代码演示也属于---组合多个查询条件:
方式1:用Specification的 and 或 or来组合多个 Specification
——每个Specification只组合一个查询条件。
需求1:查询名字和年龄都符合的条件–equal
简洁写法
需求2:查询名字是 沙 开头的,年龄大于 100的学生--------like
▲ 如何组合多个查询条件?
两种方式:
A - 用Specification的and或or来组合多个 Specification
——每个Specification只组合一个查询条件。
B - 先用CriteriaBuilder的and或or来组合多个Predicate对象,
得到一个最终的Predicate,然后再将Predicate包装成Specification。
代码演示
需求:根据传来的student,如果该对象里面的某个属性不为null,就将该属性作为查询条件进行查询。
演示:先用CriteriaBuilder的and或or来组合多个Predicate对象,
得到一个最终的Predicate,然后再将Predicate包装成Specification。
下面的查询就是组合查询,
Predicate - 代表了单个查询条件,相当于sql语句的where子句中的单个的条件
(比如 age>100,就是一个Predicate )。也可用于组合多个查询条件。
CriteriaBuilder - 专门用于构建单个Predicate。
测试结果:
完整代码:
StudentDaoTest
package cn.ljh.app.dao;
import cn.ljh.app.domain.Student;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
//SpringBootTest.WebEnvironment.NONE : 表示不需要web环境
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class StudentDaoTest
{
@Autowired
private StudentDao studentDao;
/**
* @ValueSource: 每次只能传一个参数
* @CsvSource:每次可以传多个参数
*/
//需求:查询年龄大于指定参数的记录
//参数化测试
@ParameterizedTest
@ValueSource(ints = {20, 200})
public void testFindByAgeGreaterThan(int startAge)
{
List<Student> students = studentDao.findByAgeGreaterThan(startAge);
students.forEach(System.err::println);
}
//根据年龄和班级名称查询学生
//Age 和 ClazzName 用 And 连接起来,表示两个查询条件,
//ClazzName这两个单词中间没有And连接起来,表示是一个路径写法,表示是Clazz类的name属性
@ParameterizedTest
//参数一个是int,一个是String,这个注解在传参的时候会自动进行类型转换
@CsvSource(value = {"20,超级A营", "18,超级D班"})
public void testFindByAgeAndClazzName(int age, String clazzName)
{
List<Student> students = studentDao.findByAgeAndClazzName(age, clazzName);
students.forEach(System.err::println);
}
//pageNo: 要查询哪一页的页数 , pageSize: 每页显示的条数
@ParameterizedTest
@CsvSource({"洞,2,3", "洞,1,4", "洞,3,2"})
public void testFindByAddressEndingWith(String addrSuffix, int pageNo, int pageSize)
{
//分页对象,此处的pageNo是从0开始的,0代表第一页,所以这里的 pageNo 要 -1
Pageable pageable1 = PageRequest.of(pageNo - 1, pageSize);
Page<Student> students = studentDao.findByAddressEndingWith(addrSuffix, pageable1);
int number = students.getNumber() + 1;
System.err.println("总页数:" + students.getTotalPages());
System.err.println("总条数:" + students.getTotalElements());
System.err.println("当前第:" + number + " 页");
System.err.println("当前页有:" + students.getNumberOfElements() + " 条数据");
students.forEach(System.err::println);
}
//======================================测试 Specification 查询=======================================================
//查询名字和年龄都符合的条件--equal
@ParameterizedTest
@CsvSource({"沙和尚,580"})
public void testSpecificationQuery(String name, int age)
{
/*
* root : 代表要查询的实体(就是 student)
* criteriaBuilder:专门用于构建运算符的
*/
List<Student> students = studentDao.findAll(((Specification<Student>) (root, criteriaQuery, criteriaBuilder) ->
{
//判断 root.get("name") 是否等于 name
Predicate p1 = criteriaBuilder.equal(root.get("name"), name);
return p1;
})
//再次使用 and 添加了一个 Specification 的条件
.and((root, criteriaQuery, criteriaBuilder) ->
{
Predicate p2 = criteriaBuilder.equal(root.get("age"), age);
return p2;
})
);
students.forEach(System.err::println);
}
//查询名字和年龄都符合的条件---简洁写法---equal
@ParameterizedTest
@CsvSource({"沙和尚,580"})
public void testSpecificationQuery1(String name, int age)
{
List<Student> students = studentDao.findAll(((Specification<Student>)
(root, criteriaQuery, criteriaBuilder) -> criteriaBuilder.equal(root.get("name"), name))
.and((root, criteriaQuery, criteriaBuilder) -> criteriaBuilder.equal(root.get("age"), age))
);
students.forEach(System.err::println);
}
//查询名字是 沙 开头的,年龄大于 100的学生--------like
@ParameterizedTest
@CsvSource({"猪%,100"})
public void testSpecificationQuery2(String name, int age)
{
List<Student> students = studentDao.findAll(((Specification<Student>)
(root, criteriaQuery, criteriaBuilder) -> criteriaBuilder.like(root.get("name"), name))
.and((root, criteriaQuery, criteriaBuilder) -> criteriaBuilder.gt(root.get("age"), age))
);
students.forEach(System.err::println);
}
//组合查询
@ParameterizedTest
//参数是一个对象,对象的数据从这个getStudent方法里面获取
//该方法要求:1、该方法必须以 static 修饰,
//该方法的返回值必须是 stream , Stream 中的数据必须是被测试方法要求的参数类型
@MethodSource("getStudents")
public void testSpecificationQuery3(Student student)
{
//此处查询,只要 student 的哪个属性不为null,就查询哪些条件
studentDao.findAll((Specification<Student>) (root, query, criteriaBuilder) ->
{
//使用 predicateList 来收集查询条件
List<Predicate> predicateList = new ArrayList<>();
//如果name属性不为null,就说明需要添加name作为查询条件
if (student.getName() != null && !student.getName().equals(""))
{
predicateList.add(criteriaBuilder.equal(root.get("name"), student.getName()));
}
//如果 age 属性不等于 0 ,就说明需要添加 age 作为查询条件
if (student.getAge() != 0)
{
predicateList.add(criteriaBuilder.equal(root.get("age"), student.getAge()));
}
//如果 address 属性不等于 null ,就说明需要添加 address 作为查询条件
if (student.getAddress() != null && !student.getAddress().equals(""))
{
predicateList.add(criteriaBuilder.equal(root.get("age"), student.getAge()));
}
//Gender 是char类型,如果 Gender 属性不等于 '\u0000'-->空字符串 ,就说明需要添加 Gender 作为查询条件
if (student.getGender() != '\u0000')
{
predicateList.add(criteriaBuilder.equal(root.get("gender"), student.getGender()));
}
//由于 criteriaBuilder 的 and 方法的参数是数组,因此此处将 predicateList 集合转成 数组
Predicate predicate = criteriaBuilder.and(predicateList.toArray(new Predicate[1]));
return predicate;
}).forEach(System.err::println);
}
//该方法要求:1、该方法必须以 static 修饰,
//该方法的返回值必须是 Stream , Stream 中的数据必须是被测试方法要求的参数类型
public static Stream<Student> getStudents()
{
Stream<Student> studentStream = Stream.of(
new Student("孙悟空", 0, null, '\u0000', null),
new Student("孙悟空", 500, null, '\u0000', null),
new Student("孙悟空", 500, "花果山水帘洞", '\u0000', null),
new Student("孙悟空", 500, "花果山水帘洞", '男', null),
new Student("孙", 50, "花果山", '男', null)
);
return studentStream;
}
}