JPA动态sql的使用姿势

JPA基础用法:JPA在springboot中的使用_子书少卿的博客-CSDN博客

目录

问题引出

姿势一、Query by Example

姿势二、Specification

姿势三、entityManager

姿势四、QueryDSL


问题引出

        初使用jpa时,一直使用jpql(原生sql)和规定方法名的实行来实现业务,在特定的场景下需要用到动态查询条件时,也是直接使用jpsql的形式来声明动态条件,通过数据库的动态条件来实现;如下实例:

    @Query(value = " SELECT *  FROM water.testjpa " +
            "WHERE name = ?1 " +
            "and if(?2 != '', age = ?2, 1=1)", nativeQuery = true)
    List test(String name, String age);

        通过动态传入不同的参数值,来通过sql来实现动态条件的控制,sql打印如下

Hibernate:  SELECT *  FROM water.testjpa WHERE name = ? and if(? != '', age = ?, 1=1)

         如果数据表中的数据量不大的情况下,sql执行无压力,但如果数据量达到百万级,这种通过条件判断形式的性能要比直接sql拼接具体条件性能差很多;

        那么如果实现动态生成sql成了新的问题;

姿势一、Query by Example

        一种通过操作entity实例来控制动态条件的实现方式,操作简单但缺陷也很明显,往下看↓

        使用限制:(缺陷)

                1、只支持查询

                     不支持嵌套或分组的属性约束,如 age = ?1 or (age = ?1 and name= ?2)

                     只支持字符串的start/contains/ends/regex匹配和其他属性类型的精确匹配

                     不支持 :>、<、in、between等

        使用:

        1、repository接口-追加继承接口 QueryByExampleExecutor ;

@Repository
public interface TestQueryByExampleRepository extends
        JpaRepository
        // 泛型为操作的试题类型
        ,QueryByExampleExecutor {

}

        接口原码及功能如下:

public interface QueryByExampleExecutor {
    // 根据条件查询一条数据
     Optional findOne(Example var1);
    // 根据条件查询结果集
     Iterable findAll(Example var1);
    // 根据条件查询结果集,并排序
     Iterable findAll(Example var1, Sort var2);
    // 根据条件查询结果集,并分页
     Page findAll(Example var1, Pageable var2);
    // 计数
     long count(Example var1);
    // 判断值是否存在
     boolean exists(Example var1);
}

        2、service

@Slf4j
@Service
@AllArgsConstructor(onConstructor_ = {@Autowired})
@Transactional
public class QueryByExampleService {

    /**
     * 测试方法
     */
    public void test(){
        // 通过实体对象来实现查询条件的控制
        JPAEntity jpaEntity = new JPAEntity();
        // 根据自己的查询条件给实体赋值;
        jpaEntity.setName("小黑");
        jpaEntity.setTtt(LocalDate.now());

        // 使用实体对象构建Example对象
        Example of = Example.of(jpaEntity);
        List all = testQueryByExampleRepository.findAll(of);
        System.out.println(all);
    }


    TestQueryByExampleRepository testQueryByExampleRepository;
}

        3、test

@SpringBootTest(classes = SpringbootJpaApplication.class)
@RunWith(SpringRunner.class)
public class QueryByExampleServiceTest {

    @Resource
    QueryByExampleService queryByExampleService;

    @Test
    public void test() {
        queryByExampleService.test();
    }
}

        4、直接结果sql

select jpaentity0_.id as id1_0_, jpaentity0_.address as address2_0_, jpaentity0_.age as age3_0_, jpaentity0_.name as name4_0_, jpaentity0_.phone as phone5_0_, jpaentity0_.ttt as ttt6_0_ from testjpa jpaentity0_ where jpaentity0_.ttt=? and jpaentity0_.name=?

        可以看到,sql中只有在构建实体是赋值的name和ttt条件

        5、这种方式还支持匹配器,来对条件进行设置,改造下service,添加匹配器和匹配规则

@Slf4j
@Service
@AllArgsConstructor(onConstructor_ = {@Autowired})
@Transactional
public class QueryByExampleService {

    /**
     * 测试方法
     */
    public void test(){
        // 通过实体对象来实现查询条件的控制
        JPAEntity jpaEntity = new JPAEntity();
        // 根据自己的查询条件给实体赋值;
        jpaEntity.setName("小黑");
        jpaEntity.setTtt(LocalDate.now());
        jpaEntity.setAddress("jing");

        // 构造一个匹配器,通过匹配器对条件进行设置
        ExampleMatcher exampleMatcher = ExampleMatcher.matching()
                // 设置忽略的条件(属性)---值为JPAEntity中的属性
                .withIgnorePaths("ttt")
                // 设置忽略大小写,如果不声明值,则对所有条件进行忽略大小写
//                .withIgnoreCase("address")
                // string类型的匹配,值使用String匹配器,可以通过枚举选择,这里使用的结尾匹配  注意:这个匹配是对查询所有条件的匹配
//                .withStringMatcher(ExampleMatcher.StringMatcher.ENDING)
                // 针对单个条件进行限制,两个值为:<对哪个条件>,<限制-一个函数式接口>
                // address 字段,结尾匹配 ‘jing’
//                .withMatcher("address",s->s.endsWith())
                // 或者通过ExampleMatcher的静态方法,
                // 注:当使用withMatcher对address进行设置时,上边的.withIgnoreCase("address")会失效,需要单独设置
                .withMatcher("address", ExampleMatcher.GenericPropertyMatchers.endsWith().ignoreCase())
                ;

        // 使用实体对象构建Example对象
        Example of = Example.of(jpaEntity,exampleMatcher);
        List all = testQueryByExampleRepository.findAll(of);
        System.out.println(all);
    }


    TestQueryByExampleRepository testQueryByExampleRepository;
}

        6、使用同一个test执行,直接结果如下

Hibernate: select jpaentity0_.id as id1_0_, jpaentity0_.address as address2_0_, jpaentity0_.age as age3_0_, jpaentity0_.name as name4_0_, jpaentity0_.phone as phone5_0_, jpaentity0_.ttt as ttt6_0_ from testjpa jpaentity0_ where (lower(jpaentity0_.address) like ?) and jpaentity0_.name=?

    

        如果只是字段的精确匹配或简单的匹配可以这种方式;但如果要实现更加复杂的条件,继续往下看;

姿势二、Specification

        一种通过spring data jpa提供的实现Specification的形式来自定义查询规则的方式,实现相对复杂,但可以满足一般的使用场景;但也不完美,往下看↓

        使用限制:(缺陷)

                1、不支持分组和聚合函数;(如需要则只能通过玩entityManager了,既姿势三)

        使用:

        1、repository接口-追加继承接口 JpaSpecificationExecutor ;

@Repository
public interface TestSpecificationRepository extends
        JpaRepository,
        // 泛型为操作的试题类型
        JpaSpecificationExecutor {

}

        接口原码及功能如下:功能和QueryByExampleExecutor大体一致;

public interface JpaSpecificationExecutor {
    
    Optional findOne(@Nullable Specification var1);

    List findAll(@Nullable Specification var1);

    Page findAll(@Nullable Specification var1, Pageable var2);

    List findAll(@Nullable Specification var1, Sort var2);

    long count(@Nullable Specification var1);
}

        2、service 

        条件设置可以自己根据需在criteriaBuilder中寻找

@Slf4j
@Service
@AllArgsConstructor(onConstructor_ = {@Autowired})
@Transactional
public class SpecificationService {


    public void test(){

        // 模拟入参
        JPAEntity jpaEntity = new JPAEntity();
        jpaEntity.setId(2);
        jpaEntity.setName("小黑");
        LocalDate startTime = LocalDate.of(2023,4,17);
        LocalDate endTime = LocalDate.of(2023,4,19);

        //构建一个分页实例,用于分页
        PageRequest pageRequest = PageRequest.of(0, 1);
        // findAll方法的参数需要一个Specification接口,可以直接通过匿名类的形式来实现
        Page all = testSpecificationRepository.findAll(new Specification() {

            // root :用来获取列
            // criteriaBuilder : 设置条件,如:> < in between 等
            // criteriaQuery: 组合 where  order by 等
            @Override
            public Predicate toPredicate(Root root, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder) {
                // 获取列的时候要将结果枚举参数声明为字段对应的类型
                // 获取列
                Path id = root.get("id");
                Path name = root.get("name");
                Path ttt = root.get("ttt");

                // 创建一个容器,存放要查询的条件
                List list = new ArrayList<>();

                // 如果id不为空
                if (!ObjectUtils.isEmpty(jpaEntity.getId())) {
                    // id不为空,查询id 小于 传入值
//                    list.add(criteriaBuilder.le(id, jpaEntity.getId()));
                    // id不为空,查询id 大于 传入值
                    list.add(criteriaBuilder.ge(id, jpaEntity.getId()));
                }
                // 如果name不为空
                if (!StringUtils.isEmpty(jpaEntity.getName())) {
                    // 如果name不为空  查询name 等于 传入值
                    list.add(criteriaBuilder.equal(name, jpaEntity.getName()));
                }
                // 如果起始时间不为空
                if (!ObjectUtils.isEmpty(startTime) && !ObjectUtils.isEmpty(endTime)) {
                    // 如果起始时间不为空  查询ttt 介于两个值之间的值
                    list.add(criteriaBuilder.between(ttt, startTime, endTime));
                }
                // 汇总查询条件
                Predicate and = criteriaBuilder.and(list.toArray(new Predicate[0]));
                // 创建一个降序的排序,升序使用asc
                // 降序可以按多个条件,使用List即可,参照构造方法
                Order order = criteriaBuilder.desc(id);
                // 将查询条件和排序添加组合返回
                return criteriaQuery.where(and).orderBy(order).getRestriction();
            }
            // 设置分页实例
        }, pageRequest);

        // 结果中获取分页数据
        List content = all.getContent();
        System.out.println(content);
        // 获取总条数 all中有分页中所有参数,根据需要获取
        long totalElements = all.getTotalElements();
        System.out.println(totalElements);
    }

    // 注入
    TestSpecificationRepository testSpecificationRepository;
}

        3、test

@SpringBootTest(classes = SpringbootJpaApplication.class)
@RunWith(SpringRunner.class)
public class SpecificationServiceTest {

    @Resource
    SpecificationService specificationService;

    @Test
    public void test() {
        specificationService.test();
    }
}

        结果sql如下:

第一条sql

Hibernate: select jpaentity0_.id as id1_0_, jpaentity0_.address as address2_0_, jpaentity0_.age as age3_0_, jpaentity0_.name as name4_0_, jpaentity0_.phone as phone5_0_, jpaentity0_.ttt as ttt6_0_ from testjpa jpaentity0_ where jpaentity0_.id>=2 and jpaentity0_.name=? and (jpaentity0_.ttt between ? and ?) order by jpaentity0_.id desc limit ?

第二条sql
Hibernate: select count(jpaentity0_.id) as col_0_0_ from testjpa jpaentity0_ where jpaentity0_.id>=2 and jpaentity0_.name=? and (jpaentity0_.ttt between ? and ?)
 

        注意:如果使用分页查询,如果返回的数据条数不小于分页的size则会再次执行count原条件查询总数,用于组装findAll的返回Page对象;

        如果基于性能考虑只需要根据分页条件查询结果,不需要Page对象中的参数,则这种方式是不适合的;继续往下看↓

姿势三、entityManager

        todo :待整理

姿势四、QueryDSL

        通用查询框架,是一个三方的框架,暂不整理;
          

你可能感兴趣的:(spring,boot,java,jpa)