关于Spring Data Jpa 动态查询
- 前言
- 列举所有条件组合
- JpaSpecificationExecutor
- EntityManager执行动态拼接的hql
- 利用sql语法
- 总结
前言
通常来说网站的后台管理系统对于数据的查询需要提供多种查询条件,并且要求在用户任意的选择条件进行查询:
这种情况下后端需要通过不同的条件组合对数据库进行查询,Mybatis的<if>
标签能够很好地实现这个功能:只需要在if
标签中对参数进行判空,非空则拼接where 条件即可。
但是对于Spring Data Jpa 这种高度封装的框架来说可能会稍微复杂一些,总结了几种使用Spring Data Jpa 实现动态条件数据库查询的方案,主要有以下四种:
- 智障方案:列举所有条件组合
- 局限方案:利用SpringData提供的
JpaSpecificationExecutor
接口 构建查询对象 - 常规方案:代码拼接
hql
语句,通过EntityManager执行 - 天秀方案:利用sql本身逻辑表达式拼接hql 或 sql
列举所有条件组合
这种方法简单粗暴:列举所有查询条件的所有组合情况并根据不同的近况进行不同条件的数据库查询:
//伪代码
if(name = null){
if(age = null){
findAll();
}else{
findByAge(age);
}
}else{
if(age = null){
findByName(name);
}else{
findByNameAndAge(name,age)
}
}
这种做法在参数极少的情况下才有可能被使用,3个参数就会出现6种组合,4个参数就有16种组合。组合情况会随着参数的增多出现指数式的上升,非常难以维护。
JpaSpecificationExecutor
Spring Data 提供了JpaSpecificationExecutor接口继承这个接口提供了findAll findOne等重载方法,在查询的时候可以通过代码来构建不同的查询条件,并且支持分页行为
public interface UserRepository extends
JpaRepository<User ,Long>,JpaSpecificationExecutor<User>{
}
具体的使用方式不做赘述,感兴趣的可以自行检索。
这种方法的缺陷在于:虽然同样支持多表查询,但是关联的方式非常局限(需要在实体中有外键关系映射才能使用)无法指定通过两张表的哪两个字段是用于关联的
如果出于某些原因,两张表在业务上是关联的,但是在数据库和实体中并没有通过外键和@OneToOne
OneToMany
等注解进行关联的话 ,这种Specification是无法进行关联查询的
不过在少量数据的情况下可以通过先查询一张表在拼接IN
查询来完成某些性能要求不高的查询
EntityManager执行动态拼接的hql
Spring Data Jpa底层的实现是Hibernate ,而且通过源码可以发现其实Jpa底层的数据库操作其实都是基于EntityManager来完成的,EntityManager 是 相当于Hibernate中的Session对象。我们可以从容器中获取EntityManager对象 然后根据条件动态的拼接hql语句最终 通过EntityManager对象来执行:
public class CustomBatchSubscriberDAOImpl implements CustomBatchSubscriberDAO {
@PersistenceContext
EntityManager entityManager;
@Override
public PageVO<BatchSubscribeVO> customQuery1(String name, Integer age, int pageNo, int pageSize) {
Map<String, Object> param = new HashMap<>();
String countHql = "select count(msb.id) from User u ";
String datahql = "select u from User u ";
String whereHql = "where 1=1 ";
//动态拼接where条件
if (null != name && !"".equals(name)) {
whereHql += "and u.name like :mrName ";
param.put("name ", "%" + name + "%");
}
if (null != age) {
whereHql += "and u.age =:age ";
param.put("age ", age );
}
//...
datahql = datahql + whereHql;
Query query = entityManager.createQuery(datahql);
//注入参数
for (Map.Entry<String, Object> entry : param.entrySet()) {
query = query.setParameter(entry.getKey(), entry.getValue());
}
int start = (pageNo - 1) * pageSize + 1;
query = query.setFirstResult(start).setMaxResults(pageSize);
List resultList = query.getResultList();
//查询count
countHql = countHql + whereHql;
Query countQuery = entityManager.createQuery(countHql);
for (Map.Entry<String, Object> entry : param.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
countQuery = countQuery.setParameter(entry.getKey(), entry.getValue());
}
Object count = countQuery.getSingleResult();
//组装分页对象
PageVO<BatchSubscribeVO> pageVO = new PageVO(pageNo, pageSize);
pageVO.setTotal((Long) count);
pageVO.setDataList(resultList);
return pageVO;
}
这种方案是最被经常使用的方法 , 通常在使用Spring Data Jpa来创建项目的时候会 抽取出持久层接口基类和实现来提供 基于hql的查询,而hql的拼接则放在业务层进行处理即可,所以并不会使代码变得特别更加臃肿难以维护。
利用sql语法
示例如下:
public interface UserRepository extend JpaRepository<User,Long>{
@Query(
"select u from User u "+
"where (u.name = ?1 or ?1 is null) "+
"and(u.age = ?2 or ?2 is null) "+
)
Page<User> costomSearch(String name ,Integer age ,Pageable pageable)
}
其实就是将参数拼到hql或sql里利用逻辑表达式OR来实现参数为空时条件不生效:
name参数为空 则第一个where条件为:
# name参数为空 则第一个where条件为
where (u.name = null or null is null)
可见null is null
条件永远成立,所以改条件不会生效
反之当name参数不为空的时候 第一个where条件为:
# name参数为 “李四”
where (u.name = '李四' or '李四' is null)
可见'李四' is null
这一条件永远不成立,所以只有前面的条件生效。
利用这种方式可以非常方便的实现类似于动态查询的效果,但是这种方式只能判空 , 所以在某些情况下可能需要先在业务层进行判断,不符合条件的话就把参数置为空在调用
总结
Spring Data的有点在于简化持久层开发,高度的封装在极大程度上隐藏了数据库操作的细节,当然这并不意味着无法实现灵活的数据操作。
以上几种动态查询的方法各有优缺,需要根据具体的业务场景选择最合适的方案