72、Spring Data JPA 的 Specification 动态查询

2023-09-17 14:55:37

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;
    }


}
更多推荐

设计模式-中介者模式

每次乘坐高铁出行时,我都会像这样一个问题:这么多列车都可能通过这条轨道,会不会存在冲突的可能呢?同样的,飞机的起飞和降落时对于道路的选择也会有冲突的可能。这些情况都会造成可怕的后果,而阻止这种情况发生的就是机场调度中心。飞机在起飞和降落前都会请求机场调度中心,由机场调度中心来负责协调飞机、地面道路、摆渡车辆等。因此,机

剑指offer(C++)-JZ67:把字符串转换成整数atoi(算法-模拟)

作者:翟天保Steven版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处题目描述:写一个函数StrToInt,实现把字符串转换成整数这个功能。不能使用atoi或者其他类似的库函数。传入的字符串可能有以下部分组成:1.若干空格2.(可选)一个符号字符('+'或'-')3.数字,字母,符号,空格

线性代数的本质(十)——矩阵分解

文章目录矩阵分解LU分解QR分解特征值分解奇异值分解奇异值分解矩阵的基本子空间奇异值分解的性质矩阵的外积展开式矩阵分解矩阵的因式分解是把矩阵表示为多个矩阵的乘积,这种结构更便于理解和计算。LU分解设AAA是m×nm\timesnm×n矩阵,若AAA可以写成乘积A=LUA=LUA=LU其中,LLL为mmm阶下三角方阵,主

CodeArts Check代码检查服务用户声音反馈集锦(4)

作者:gentle_zhou原文链接:CodeArtsCheck代码检查服务用户声音反馈集锦(4)-云社区-华为云CodeArtsCheck(原CodeCheck),是自主研发的代码检查服务。建立在华为30年自动化源代码静态检查技术积累与企业级应用经验的沉淀之上,为用户提供代码风格、通用质量与网络安全风险等丰富的检查能

【智慧工地源码】智慧工地助力数字建造、智慧建造、安全建造、绿色建造

智慧工地围绕建设过程管理,建设项目与智能生产、科学管理建设项目信息生态系统集成在一起,该数据在虚拟现实环境中,将物联网收集的工程信息用于数据挖掘和分析,提供过程趋势预测和专家计划,实现工程建设的智能化管理,提高工程管理信息水平,逐步实现绿色建设和生态建设。一、施工现场智能化管理物联网智慧工地通过手机/PAD自动感应或采

Python爬虫

一、保存数据到Excelfrombs4importBeautifulSoup#网页解析,获取数据importre#正则表达式,进行文字匹配importurllib.request,urllib.error#制定URL,获取网页数据importxlwt#进行excel操作importsqlite3#进行SQLite数据库

【案例+源码】数据可视化之统计绘图-Seaborn全套教程

数据可视化-Seaborn简易入门Matplotlib试着让简单的事情更加简单,困难的事情变得可能,而Seaborn就是让困难的东西更加简单。seaborn是针对统计绘图的,一般来说,seaborn能满足数据分析90%的绘图需求。Seaborn其实是在matplotlib的基础上进行了更高级的API封装,从而使得作图更

前端JavaScript中requestAnimationFrame:优化动画和渲染的利器

🎬岸边的风:个人主页🔥个人专栏:《VUE》《javaScript》⛺️生活的理想,就是为了理想的生活!目录引言1.requestAnimationFrame简介2.requestAnimationFrame的属性3.requestAnimationFrame的应用场景3.1动画效果3.2游戏开发3.3数据可视化3.

SpringBoot结合Vue.js+axios框架实现增删改查功能+网页端实时显示数据库数据(包括删除多条数据)

本文适用对象:已有基础的同学,知道基础的SpringBoot配置和Vue操作。在此基础上本文实现基于SpringBoot和Vue.js基础上的增删改查和数据回显、刷新等。一、实时显示数据库数据实现步骤:第1步:编写动态请求响应类:在启动类同父目录下创建controller包,在包下创建DataController类,添

Compose的一些小Tips - 可组合项的绘制

系列文章Compose的一些小Tips-可组合项的生命周期Compose的一些小Tips-可组合项的绘制(本文)Compose的一些小Tips-列表的优化前言本系列介绍Compose的一些常识,了解这些tips并不会让人摇身一变成为大佬,但可以帮助到一些学习Compose的安卓开发者避免一些误区,也是对Compose入

惯性动捕+数据手套,让“虚拟”触手可及

当今,虚拟现实技术已经从科幻电影走进现实生活。在数字化时代,惯性动作捕捉系统与数据手套的结合使用,带给我们全新的虚拟互动体验,使虚拟世界更能够“触手可及”。01惯性动作捕捉系统FOHEARTMAGIC是一款高性能的惯性动作捕捉系统。它由17个惯性传感器和数据接收器组成。每个惯性传感器都内置了三轴加速度计、三轴磁力计和三

热文推荐