• 72、Spring Data JPA 的 Specification 动态查询


    Specification:规范、规格

    ★ Specification查询

    它也是Spring Data提供的查询——是对JPA本身 Criteria 动态查询 的包装。
    
    • 1

    ▲ 为何要有动态查询

    页面上常常会让用户添加不同的查询条件,程序就需要根据用户输入的条件,动态地组合不同的查询条件。
    
    JPA为动态查询提供了Criteria查询支持。
    
    Spring Data JPA则对Criteria查询进行了封装,封装之后的结果就是Specification查询。
    
    ——Specification查询比Jpa的Criteria动态查询更加简单。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如图:
    在这里插入图片描述

    ▲ 核心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的问题,
        而改为使用面向对象的方式来组合查询条件。
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ▲ 如何创建Specification对象(用于组合多个查询条件)

    - Specification参数用于封装多个代表查询条件的Predicate对象。
    
    - Specification接口只定义了一个toPredicate()方法,
      该方法返回的Predicate对象就是Specification查询的查询条件,
    
      程序通常使用Lambda表达式来实现toPredicate()方法来定义动态查询条件。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    ▲ 还涉及如下两个API(本身就是来自于JPA的规范)

    Predicate -  代表了单个查询条件,相当于sql语句的where子句中的单个的条件
    (比如 age>100,就是一个Predicate )。
    也可用于组合多个查询条件。
    
    
    CriteriaBuilder - 专门用于构建单个Predicate。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    代码演示

      下面的代码演示也属于---组合多个查询条件:
      方式1:用Specification的 and 或 or来组合多个 Specification
          ——每个Specification只组合一个查询条件。
    
    • 1
    • 2
    • 3

    需求1:查询名字和年龄都符合的条件–equal
    在这里插入图片描述

    简洁写法
    在这里插入图片描述

    需求2:查询名字是 沙 开头的,年龄大于 100的学生--------like
    在这里插入图片描述

    ▲ 如何组合多个查询条件?

     两种方式:
    
      A - 用Specification的and或or来组合多个 Specification
          ——每个Specification只组合一个查询条件。
    
      B - 先用CriteriaBuilder的and或or来组合多个Predicate对象,
          得到一个最终的Predicate,然后再将Predicate包装成Specification。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    代码演示

    需求:根据传来的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;
        }
    
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
  • 相关阅读:
    数据结构:栈和队列
    【mysql集群故障恢复】
    测试工具介绍||Postman的简单使用
    【论文合集】2022年12月医学影像期刊论文合集
    实现CenterNet图像分割算法模型的转换和量化(SDK0301-转ONNX编译)
    05. 聚类---K(k-means)均值
    第三章 内存管理 六、基本分页存储管理
    【C++】近期知识点杂记(getline、cin、nullptr与C语言风格的字符串、stoi函数、new)
    GitLab先有的用户 ,后期ldap介入,gitlab用户问题
    关于电影的HTML网页设计-威海影视网站首页-电影主题HTM5网页设计作业成品
  • 原文地址:https://blog.csdn.net/weixin_44411039/article/details/132920430