目录
使用Specification来进行动态条件分页查询案例演示
下面将介绍三种可以实现动态查询的方式,实际上它们各自都有自己的应用场景,每一种方法都有自己的局限。
只支持查询:
不支持嵌套或分组的属性约束,如 firstname = ?0 or (firstname = ?1 and lastname = ?2).
【只支持字符串】 start/contains/ends/regex 匹配和其他属性类型的精确匹配。
使用步骤:
1、将Repository继承QueryByExampleExecutor
- public interface CustomerQBERepository extends PagingAndSortingRepository
, QueryByExampleExecutor { //这个泛型表示的是你要操作的Java实体类 -
- }
我们看一下这个querybyexampleexecutor的源码定义了哪些方法:
- public interface QueryByExampleExecutor
{ -
extends T> Optional findOne(Example example); -
-
extends T> Iterable findAll(Example example); -
-
extends T> Iterable findAll(Example example, Sort sort); -
-
extends T> Page findAll(Example example, Pageable pageable); -
-
extends T> long count(Example example); -
-
extends T> boolean exists(Example example); -
-
extends T, R> R findBy(Example example, Function, R> queryFunction) ; - }
方法演示:
- @Autowired
- CustomerQBERepository repository; //把刚刚自己定义的接口注入到容器
-
-
- //根据客户名称 客户地址动态查询
-
- @Test
- public void test01(){
-
- //构建实体类需要动态查询的条件 比如需要通过名字和地址进行动态查询
- Customer customer=new Customer();
- customer.setCustName("徐庶"); //在实际开发中这个查询的条件都是从前端传过来的,比如传了name和address过来,你需要使用对应的数据类型进行接收就行,这里是演示所以直接写死了查询条件
- customer.setCustAddress("BEIJING");
-
- // 通过Example构建查询条件 特别注意这个Example对象是spring data中的依赖,别引成了hibernate的
- Example
example = Example.of(customer); -
- List
list = (List) repository.findAll(example); - System.out.println(list);
- }
-
-
-
- /**
- * 通过匹配器进行条件的限制 客户名称 客户地址动态查询
- */
- @Test
- public void test02(){
-
- // 查询条件
- Customer customer=new Customer();
- customer.setCustName("庶");
- customer.setCustAddress("JING");
-
- // 通过匹配器 对条件行为进行设置
- ExampleMatcher matcher = ExampleMatcher.matching()
- //.withIgnorePaths("custName") // 设置忽略的属性
- //.withIgnoreCase("custAddress") // 设置忽略大小写
- //.withStringMatcher(ExampleMatcher.StringMatcher.ENDING); // 对所有条件字符串进行了结尾匹配
- .withMatcher("custAddress",m -> m.endsWith().ignoreCase()); // 针对单个条件进行限制, 会使withIgnoreCase失效,需要单独设置,第一个参数是需要对那个属性进行匹配,第二个参数表示的是需要对属性使用什么样的匹配规则
- //.withMatcher("custAddress",ExampleMatcher.GenericPropertyMatchers.endsWith().ignoreCase());
-
- // 通过Example构建查询条件 加了匹配器的查询条件
- Example
example = Example.of(customer,matcher); -
- List
list = (List) repository.findAll(example); - System.out.println(list);
- }
匹配器提供的一些方法:

注意演示案例使用的持久层框架是spring data jpa中的hibernate。
pom文件依赖:我的演示环境的spring boot是 2.6.10
- <dependencies>
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-data-jpaartifactId>
- dependency>
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-webartifactId>
- dependency>
-
- <dependency>
- <groupId>mysqlgroupId>
- <artifactId>mysql-connector-javaartifactId>
- <scope>runtimescope>
- dependency>
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-testartifactId>
- <scope>testscope>
- dependency>
- dependencies>
1、创建实体类
- package com.ljm.entity;
- import javax.persistence.*;
- import java.util.Objects;
-
- /**
- * @author LJM
- * @create 2022/8/1
- */
- @Entity
- @Table(name = "student")
- public class Student {
-
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- @Column(name = "id")
- private Long id;
-
-
- @Column(name = "name")
- private String name;
-
-
- @Column(name = "age")
- private Long age;
-
- @Column(name = "loves")
- private String loves;
-
-
- @Column(name = "card_num")
- private String cardNum;
-
-
- public void setId(Long id) {
- this.id = id;
- }
-
- public Long getId() {
- return this.id;
- }
-
-
- public void setName(String name) {
- this.name = name;
- }
-
-
- public String getName() {
- return this.name;
- }
-
- public void setAge(Long age) {
- this.age = age;
- }
-
- public Long getAge() {
- return this.age;
- }
-
- public void setLoves(String loves) {
- this.loves = loves;
- }
-
-
- public String getLoves() {
- return this.loves;
- }
-
- public String getCardNum() {
- return cardNum;
- }
-
- public void setCardNum(String cardNum) {
- this.cardNum = cardNum;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Student student = (Student) o;
- return Objects.equals(id, student.id) && Objects.equals(name, student.name) && Objects.equals(age, student.age) && Objects.equals(loves, student.loves) && Objects.equals(cardNum, student.cardNum);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(id, name, age, loves, cardNum);
- }
-
- @Override
- public String toString() {
- return "Student{" +
- "id='" + id + '\'' +
- ", name='" + name + '\'' +
- ", age=" + age +
- ", loves='" + loves + '\'' +
- ", cardNum='" + cardNum + '\'' +
- '}';
- }
- }
-
2、数据库创建表(实际上hibernate会帮我们自动创建,只要我们在配置文件中提供库名就行),
或者是使用sql:
- SET NAMES utf8mb4;
- SET FOREIGN_KEY_CHECKS = 0;
-
- -- ----------------------------
- -- Table structure for student
- -- ----------------------------
- DROP TABLE IF EXISTS `student`;
- CREATE TABLE `student` (
- `id` bigint NOT NULL AUTO_INCREMENT,
- `age` bigint NULL DEFAULT NULL,
- `card_num` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
- `loves` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
- `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
- PRIMARY KEY (`id`) USING BTREE
- ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;
-
- -- ----------------------------
- -- Records of student
- -- ----------------------------
- INSERT INTO `student` VALUES (1, 16, '12345', '打篮球', '小李');
- INSERT INTO `student` VALUES (2, 16, '123456', '测试', '小王');
- INSERT INTO `student` VALUES (3, 17, '123456', '测试', '小王');
-
- SET FOREIGN_KEY_CHECKS = 1;
3、创建dao层
- package com.ljm.dao;
-
- import com.ljm.entity.Student;
- import org.springframework.data.jpa.repository.JpaRepository;
- import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
-
- public interface StudentRepository extends JpaRepository
, JpaSpecificationExecutor { -
- }
创建实体类和数据库表相关步骤在上面;
4、直接编写service层具体的实现,这里就不编写service接口了。
- @Service
- public class StudentService {
-
- @Autowired
- private StudentRepository studentRepository;
-
- public Page
queryStudent(Student queryStudent, Pageable pageable) { -
- //创建匹配条件
- ExampleMatcher matcher = ExampleMatcher.matching().withIgnoreNullValues();
-
- Example
example = Example.of(queryStudent, matcher); - Page
pageStudent = studentRepository.findAll(example, pageable); - return pageStudent ;
- }
- }
5、编写controller
- @RestController
- @RequestMapping("/student")
- public class StudentController {
-
- @Autowired
- StudentService studentService;
-
- @GetMapping("/list")
- public Page
list(int page, int size, @RequestBody Student student) { -
- PageRequest pageRequest = PageRequest.of(page, size);
- Page
pageStudent = - studentService.queryStudent(student, pageRequest);
- return pageStudent;
-
- }
- }

之前使用Query by Example只能针对字符串进行条件设置,那如果希望对所有类型支持,可以使用Specifications。
1、继承接口JpaSpecificationExecutor。
- public interface CustomerSpecificationsRepository
- extends PagingAndSortingRepository
, - JpaSpecificationExecutor
{ //这个泛型表示的是你要操作的Java实体类 -
- }
2、传入Specification的实现: 结合lambda表达式
Root:查询哪个表(关联查询) = from
CriteriaQuery:查询哪些字段,排序是什么 =组合(order by . where )
CriteriaBuilder:条件之间是什么关系,如何生成一个查询条件,每一个查询条件都是什么类型(> between in...) = where
Predicate(Expression): 每一条查询条件的详细描述
root对象可以让我们从表中获取我们想要的列
CriteriaBuilder where 设置各种条件 (> < in between......)
query 组合(order by , where)
- @Autowired
- CustomerSpecificationsRepository repository; //把刚刚自定义的接口注入到容器中
-
-
- //进行精确匹配,单一条件查询
- @Test
- public void testR2(){
-
- List
customer = repository.findAll(new Specification() { - @Override
- public Predicate toPredicate(Root
root, CriteriaQuery> query, CriteriaBuilder cb) { -
- // root对象可以让我们从表中获取我们想要的列
- // CriteriaBuilder where 设置各种条件 (> < in ..)
- // query 组合(order by , where)
- Path
- Path
- Path
-
- // 对字段进行精确匹配 参数1 :为哪个字段设置条件 参数2:值
- Predicate predicate = cb.equal(custAddress, "BEIJING");
-
- return predicate;
- }
- });
-
- System.out.println(customer);
- }
-
-
-
- //进行多条件查询
- @Test
- public void testR3(){
-
- List
customer = repository.findAll(new Specification() { - @Override
- public Predicate toPredicate(Root
root, CriteriaQuery> query, CriteriaBuilder cb) { -
- //root对象可以让我们从表中获取我们想要的列
- // CriteriaBuilder where 设置各种条件 (> < in ..)
- // query 组合(order by , where)
- Path
custId = root.get("custId"); - Path
custName = root.get("custName"); - Path
custAddress = root.get("custAddress"); -
- // 参数1 :为哪个字段设置条件 参数2:值
- Predicate custAddressP = cb.equal(custAddress, "BEIJING"); //等于
- Predicate custIdP = cb.greaterThan(custId, 0L); //大于
- CriteriaBuilder.In
in = cb.in(custName); //in - in.value("徐庶").value("王五"); //通过点value来进行in范围的拼接
-
- //对多个条件进行拼接
- Predicate and = cb.and(custAddressP, custIdP,in);
-
- return and;
- }
- });
-
- System.out.println(customer);
- }
-
- //---------------------------------------------------------------------------------------------------------
- //注意上面我们进行查询条件的构建的时候,是把查询条件给写死了,在实际开发中这个条件应该是动态的,我们需要根据前端传来的数据进行判断,然后通过判断的结果来进行查询条件的构建---主要是进行非空判断
-
-
- //模拟动态条件查询
- @Test
- public void testR4(){
-
- //模拟前端传过来的数据
- Customer params=new Customer();
- //params.setCustAddress("BEIJING");
- params.setCustId(0L);
- params.setCustName("徐庶,王五");
-
- List
customer = repository.findAll(new Specification() { - @Override
- public Predicate toPredicate(Root
root, CriteriaQuery> query, CriteriaBuilder cb) { -
-
- //root对象可以让我们从表中获取我们想要的列
- // CriteriaBuilder where 设置各种条件 (> < in ..)
- // query 组合(order by , where)
-
- // 1. 通过root拿到需要设置条件的字段
- Path
custId = root.get("custId"); - Path
custName = root.get("custName"); - Path
custAddress = root.get("custAddress"); -
- // 2. 通过CriteriaBuilder设置不同类型条件
- //2.1 因为动态查询的时候条件是变化的,不确定的,所以需要使用集合来进行条件的保存
- List
list=new ArrayList<>(); - if(!StringUtils.isEmpty(params.getCustAddress())) {
- // 参数1 :为哪个字段设置条件 参数2:值
- list.add(cb.equal(custAddress, "BEIJING")) ;
- }
- if(params.getCustId()>-1){
- list.add(cb.greaterThan(custId, 0L));
- }
-
- if(!StringUtils.isEmpty(params.getCustName())) {
- CriteriaBuilder.In
in = cb.in(custName); - in.value("徐庶").value("王五");
- list.add(in);
- }
-
-
- // 组合条件 因为涉及动态查询,这个拼接的查询条件个数在上面的判断中已经确定了,这里需要我们传一个数组过来,通过集合转数组的方法进行转换,不过需要的是Prediccate类型的定长数组
- Predicate and = cb.and(list.toArray(new Predicate[list.size()]));
-
- return and;
- }
- });
-
- System.out.println(customer);
- }
-
-
- //进行排序等多条件的操作
- @Test
- public void testR5(){
-
- Customer params=new Customer();
- //params.setCustAddress("BEIJING");
- params.setCustId(0L);
- params.setCustName("徐庶,王五");
-
- List
customer = repository.findAll(new Specification() { - @Override
- public Predicate toPredicate(Root
root, CriteriaQuery> query, CriteriaBuilder cb) { -
- //root对象可以让我们从表中获取我们想要的列
- // CriteriaBuilder where 设置各种条件 (> < in ..)
- // query 组合(order by , where)
- Path
custId = root.get("custId"); - Path
custName = root.get("custName"); - Path
custAddress = root.get("custAddress"); -
- // 参数1 :为哪个字段设置条件 参数2:值
- List
list=new ArrayList<>(); - if(!StringUtils.isEmpty(params.getCustAddress())) {
- list.add(cb.equal(custAddress, "BEIJING")) ;
- }
- if(params.getCustId()>-1){
- list.add(cb.greaterThan(custId, 0L));
- }
-
- if(!StringUtils.isEmpty(params.getCustName())) {
- CriteriaBuilder.In
in = cb.in(custName); - in.value("徐庶").value("王五");
- list.add(in);
- }
-
-
- Predicate and = cb.and(list.toArray(new Predicate[list.size()]));
-
- //对id字段进行降序排序
- Order desc = cb.desc(custId);
-
- //使用呢query对象对条件和组合进行拼接
- return query.where(and).orderBy(desc).getRestriction();
- }
- });
-
- System.out.println(customer);
- }
缺点:不支持分组等相关聚合函数的操作,但是支持排序。 如果你还是想要通过jpa实现分组等聚合函数的操作,那就需要通过原生的jpa,通过自己获取root对象,CriteriaQuery和CriteriaBuilder对象,然后再使用他们继续操作。因为重写toPredicate方法提供的数据库操作是定死的字段,你想要修改就只能提供原生的jpa操作来自己实现。
- @Autowired
- private StudentRepository studentRepository;
-
- /**
- * 通过Specification构建动态查询条件,动态的条件为通过name,age,学号进行动态查询
- * @param student 前端传过来的查询dto
- * @return specification 返回构建好的动态查询条件
- */
- public Specification
createSpecification(Student student) { - Specification
specification = new Specification() { - @Override
- public Predicate toPredicate(Root root, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder) {
- //构建查询条件
- Path
name = root.get("name"); - Path
age = root.get("age"); - Path
cardNum = root.get("cardNum"); -
- ArrayList
list = new ArrayList<>(); - if (!StringUtils.isEmpty(student.getName())) {
- list.add(criteriaBuilder.equal(name, student.getName()));
- }
- //这个age这里会抛空指针异常(前端可能会传null过来),暂时就想到了使用这种方法进行了处理。。。。
- Long aLong = student.getAge();
- String sLong = String.valueOf(aLong);
- if (!"null".equals(sLong)){
- long ageLong = Long.parseLong(sLong);
- if ( ageLong > 0 || ageLong < 150) {
- list.add(criteriaBuilder.equal(age, ageLong));
- }
- }
-
- if (!StringUtils.isEmpty(student.getCardNum())) {
- list.add(criteriaBuilder.equal(cardNum, student.getCardNum()));
- }
-
- Predicate and = criteriaBuilder.and(list.toArray(new Predicate[list.size()]));
-
- return and;
- }
-
- };
-
- return specification;
- }
-
-
-
- /**
- * 通过Specification进行条件分页查询,查询条件包含姓名、年龄、学号三个字段
- * @param currentPage 当前页面
- * @param pageSize 每页存放的数据条数
- * @param specification Specification对象
- * @return studentPage 返回分页查询结果
- */
- public Page
page (Integer currentPage, Integer pageSize, Specification specification) { -
- Pageable pageable = PageRequest.of((currentPage - 1) > 0 ? (currentPage - 1) : 0, pageSize);
- Page
studentPage = studentRepository.findAll(specification, pageable); - return studentPage;
- }
编写controller:
- @Autowire
- StudentService studentService
-
- /**
- * 通过Specification进行动态条件的分页查询,
- * 动态分页查询条件为:name,age,学号
- * @param page 当前页
- * @param size 每页存放的数据条数
- * @param student 分页查询结果
- * @return
- */
- @GetMapping("/page")
- public Page
page( - @RequestParam(value = "page", required = false, defaultValue = "1") Integer page,
- @RequestParam(value = "size", required = false, defaultValue = "10") Integer size,
- @RequestBody Student student){
-
- Specification
specification = studentService.createSpecification(student); -
- Page
studentPage = studentService.page(page, size, specification); -
- return studentPage;
-
- }

QueryDSL是基于ORM框架或SQL平台上的一个通用查询框架。借助QueryDSL可以在任何支持的ORM框架或SQL平台上以通用API方式构建查询。 JPA是QueryDSL的主要集成技术,是JPQL和Criteria查询的代替方法。目前QueryDSL支持的平台包括JPA,JDO,SQL,Mongodb 等等。
Querydsl扩展能让我们以链式方式代码编写查询方法。该扩展需要一个接口QueryDslPredicateExecutor,它定义了很多查询方法。
我们自己定义的接口继承QueryDslPredicateExecutor该接口,那么我们就可以使用该接口提供的各种方法了。
- public interface CustomerQueryDSLRepository extends
- PagingAndSortingRepository
- , QuerydslPredicateExecutor
{ //这个泛型是我们需要操作的Java的实体类 -
- }
需要引入单独的依赖:
- <querydsl.version>4.4.0querydsl.version>
-
- <dependency>
- <groupId>com.querydslgroupId>
- <artifactId>querydsl‐jpaartifactId>
- <version>${querydsl.version}version>
- dependency>
引入组件:用于构建q类,但是加入这个组件后它不会自动编译,所以需要我们在maven的组件那里点击进行编译。

- <apt.version>1.1.3apt.version>
-
-
- <plugins>
- <plugin>
- <groupId>com.mysema.mavengroupId>
- <artifactId>apt-maven-pluginartifactId>
- <version>${apt.version}version>
- <dependencies>
- <dependency>
- <groupId>com.querydslgroupId>
- <artifactId>querydsl-aptartifactId>
- <version>${querydsl.version}version>
- dependency>
- dependencies>
- <executions>
- <execution>
- <phase>generate-sourcesphase>
- <goals>
- <goal>processgoal>
- goals>
- <configuration>
- <outputDirectory>target/generated-sources/queriesoutputDirectory>
- <processor>com.querydsl.apt.jpa.JPAAnnotationProcessorprocessor>
- <logOnlyOnError>truelogOnlyOnError>
- configuration>
- execution>
- executions>
- plugin>
- plugins>
编译完成后会在我们的target目录下生成对应的class文件,这样我们就可以自己去构建q类去了?实际上还是不行,因为这个编译后的只是一个class文件,这个时候我们书写代码去用这个Q类还是会报错,应该是压根点不出来。这是因为我们编写代码是用source文件夹进行编写的,而字节码文件是被排除在source(可以进行编码的文件夹)目录下的,所以这个需要我们手动的把这个Q类所在的文件夹变成source文件夹。


然后就可以进行编码测试了:
- @Autowired
- CustomerQueryDSLRepository repository; //把刚刚自定义的接口注入容器中
-
- @Test
- public void test01() {
- //拿到Q类对象
- QCustomer customer = QCustomer.customer;
- // 通过Id查找 这里的id我们写死为1了,但是实际开发中是会用一个变量来接收前端传输过来的id的
- BooleanExpression eq = customer.custId.eq(1L);
- System.out.println(repository.findOne(eq));
- }
-
-
- /**
- * 查询客户名称范围 (in)
- 等于 EQ : equal .eq
- 不等于 NE : not equal .ne
- 小于 LT : less than .lt
- 大于 GT : greater than .gt
- 小于等于 LE : less than or equal .loe
- 大于等于 GE : greater than or equal .goe
- * id >大于
- * 地址 精确
- */
- @Test
- public void test02() {
- QCustomer customer = QCustomer.customer;
-
- // 通过Id查找
- BooleanExpression and = customer.custName.in("徐庶", "王五")
- .and(customer.custId.gt(0L)) //id大于0
- .and(customer.custAddress.eq("BEIJING")); //地址为BEIJING
-
- System.out.println(repository.findOne(and));
-
- }
-
-
-
- //模拟动态查询
- @Test
- public void test03() {
-
- Customer params=new Customer();
- params.setCustAddress("BEIJING");
- params.setCustId(0L);
- params.setCustName("徐庶,王五");
-
-
- QCustomer customer = QCustomer.customer;
-
- // 初始条件 类似于1=1 永远都成立的条件
- BooleanExpression expression = customer.isNotNull().or(customer.isNull());
-
- //这里是使用三目运算来进行判断了,当然也可以使用if来进行判断
- //注意:记得要使用and来进行拼接这个条件
- expression=params.getCustId()>-1?
- expression.and(customer.custId.gt(params.getCustId())):expression;
- expression=!StringUtils.isEmpty( params.getCustName())?
- expression.and(customer.custName.in(params.getCustName().split(","))):expression;
- expression=!StringUtils.isEmpty( params.getCustAddress())?
- expression.and(customer.custAddress.eq(params.getCustAddress())):expression;
-
-
- System.out.println(repository.findAll(expression));
-
- }
-
-
- // 解决线程安全问题 如果使用@autowire来对EntityManager进行注入,那可能会出现线程安全问题,使用注解PersistenceContext可以为每一个线程单独绑定一个EntityManager,这样就可以解决线程不安全的问题了
- @PersistenceContext
- EntityManager em;
-
- /**
- * 自定义列查询、分组
- * 需要使用原生态的方式(Specification)
- * 通过Repository进行查询, 列、表都是固定
- */
- @Test
- public void test04() {
- JPAQueryFactory factory = new JPAQueryFactory(em);
-
- QCustomer customer = QCustomer.customer;
-
- // 构建基于QueryDSL的查询 像写SQL一样来拼接条件 这个Tuple是一个自定义对象,主要是负责来接收你要查询信息,比如你只需要查一张表中的两个字段,实际上是没有对象来接收这个查询结果的两个字段的,而提供的Tuple这个对象就可以用来接收这种类型的数据
- JPAQuery
tupleJPAQuery = factory.select(customer.custId, customer.custName) - .from(customer)
- .where(customer.custId.eq(1L))
- .orderBy(customer.custId.desc());
-
- // 执行查询
- List
fetch = tupleJPAQuery.fetch(); -
- // 处理返回数据 对结果集进行遍历
- for (Tuple tuple : fetch) {
- System.out.println(tuple.get(customer.custId));
- System.out.println(tuple.get(customer.custName));
- }
-
- }
-
-
- //结合聚合函数进行查询,注意结果集的返回值
- @Test
- public void test05() {
- JPAQueryFactory factory = new JPAQueryFactory(em);
- QCustomer customer = QCustomer.customer;
-
- // 构建基于QueryDSL的查询
- JPAQuery
longJPAQuery = factory.select( - customer.custId.sum())
- .from(customer)
- //.where(customer.custId.eq(1L))
- .orderBy(customer.custId.desc());
-
- // 执行查询
- List
fetch = longJPAQuery.fetch(); -
- // 处理返回数据
- for (Long sum : fetch) {
- System.out.println(sum);
- }
-
- }