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<JPAEntity> 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 extendsJpaRepository<JPAEntity, Long>// 泛型为操作的试题类型,QueryByExampleExecutor<JPAEntity> {}
接口原码及功能如下:
public interface QueryByExampleExecutor<T> {// 根据条件查询一条数据<S extends T> Optional<S> findOne(Example<S> var1);// 根据条件查询结果集<S extends T> Iterable<S> findAll(Example<S> var1);// 根据条件查询结果集,并排序<S extends T> Iterable<S> findAll(Example<S> var1, Sort var2);// 根据条件查询结果集,并分页<S extends T> Page<S> findAll(Example<S> var1, Pageable var2);// 计数<S extends T> long count(Example<S> var1);// 判断值是否存在<S extends T> boolean exists(Example<S> 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<JPAEntity> of = Example.of(jpaEntity);List<JPAEntity> all = testQueryByExampleRepository.findAll(of);System.out.println(all);}TestQueryByExampleRepository testQueryByExampleRepository;
}
3、test
@SpringBootTest(classes = SpringbootJpaApplication.class)
@RunWith(SpringRunner.class)
public class QueryByExampleServiceTest {@ResourceQueryByExampleService queryByExampleService;@Testpublic 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<JPAEntity> of = Example.of(jpaEntity,exampleMatcher);List<JPAEntity> 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 extendsJpaRepository<JPAEntity, Long>,// 泛型为操作的试题类型JpaSpecificationExecutor<JPAEntity> {}
接口原码及功能如下:功能和QueryByExampleExecutor大体一致;
public interface JpaSpecificationExecutor<T> {Optional<T> findOne(@Nullable Specification<T> var1);List<T> findAll(@Nullable Specification<T> var1);Page<T> findAll(@Nullable Specification<T> var1, Pageable var2);List<T> findAll(@Nullable Specification<T> var1, Sort var2);long count(@Nullable Specification<T> 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<JPAEntity> all = testSpecificationRepository.findAll(new Specification<JPAEntity>() {// root :用来获取列// criteriaBuilder : 设置条件,如:> < in between 等// criteriaQuery: 组合 where order by 等@Overridepublic Predicate toPredicate(Root<JPAEntity> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {// 获取列的时候要将结果枚举参数声明为字段对应的类型// 获取列Path<Integer> id = root.get("id");Path<String> name = root.get("name");Path<LocalDate> ttt = root.get("ttt");// 创建一个容器,存放要查询的条件List<Predicate> 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 order = criteriaBuilder.desc(id);// 将查询条件和排序添加组合返回return criteriaQuery.where(and).orderBy(order).getRestriction();}// 设置分页实例}, pageRequest);// 结果中获取分页数据List<JPAEntity> 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 {@ResourceSpecificationService specificationService;@Testpublic 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
通用查询框架,是一个三方的框架,暂不整理;