描述
一直以来动态查询都是Jpa
的一个痛点。最近做的项目使用Spring Data Jpa
作为持久层,每次碰见动态查询都很头疼,所以就想着封装一个工具用来处理动态查询。
思路
定义一个工具类,在工具类中动态拼接sql
条件,最后使用JdbcTemplate
执行拼接好的sql
。
实现
定义工具类
定义一个类DynamicQueryUtil
,并在DynamicQueryUtil
类中定义一个StringBuilder
类型的属性sqlBuilder
。sqlBuilder
属性用于拼接sql
字符串。
/**
* 动态查询工具
*
* @author 不可食用盐
*/
@Slf4j
public class DynamicQueryUtil {
private StringBuilder sqlBuilder;
// 提供静态方法创建DynamicQueryUtil
public static DynamicQueryUtil of(String sql) {
if (StrUtil.isBlank(sql)){
throw new RuntimeException("sql不能为空");
}
DynamicQueryUtil dynamicQueryUtil = new DynamicQueryUtil();
dynamicQueryUtil.sqlBuilder = new StringBuilder(sql);
return dynamicQueryUtil;
}
// 私有化构造方法,防止直接创建
private DynamicQueryUtil(){}
}
定义拼接参数的方法
定义动态拼接参数的方法,思路即使参数不为空的时候,把对应的查询条件拼接在sqlBuilder
中。其中
/**
* 动态追加查询条件
*
* @param condition 条件
* @param value 条件值
* @author 不可食用盐
*/
public DynamicQueryUtil dynamicAppendCondition(String condition, String value) {
if (StrUtil.isNotBlank(value)) {
this.sqlBuilder.append(condition).append(" '").append(value).append("' ");
}
return this;
}
/**
* 动态追加查询条件
*
* @param condition 条件
* @param value 条件值
* @author 不可食用盐
*/
public DynamicQueryUtil dynamicAppendCondition(String condition, Number value) {
if (ObjectUtil.isNotNull(value)) {
this.sqlBuilder.append(condition).append(value);
}
return this;
}
/**
* 动态追加查询条件
*
* @param condition 条件
* @author 不可食用盐
*/
public DynamicQueryUtil dynamicAppendCondition(String condition) {
if (StrUtil.isNotBlank(condition)) {
this.sqlBuilder.append(condition);
}
return this;
}
/**
* 动态追加查询条件
*
* @param condition
* @param rList
* @author 不可食用盐
*/
public DynamicQueryUtil dynamicAppendStrIn(String condition, List<String> rList) {
if (ObjectUtil.isNotEmpty(rList)) {
String s1 = rList.stream().map(s -> " '" + s + "'").collect(Collectors.joining(","));
this.sqlBuilder.append(condition).append(" ( ").append(s1).append(" ) ");
}
return this;
}
/**
* 动态追加查询条件
*
* @param condition
* @param rList
* @author 不可食用盐
*/
public DynamicQueryUtil dynamicAppendNumIn(String condition, List<Number> rList) {
if (ObjectUtil.isNotEmpty(rList)) {
String s1 = rList.stream().map(String::valueOf).collect(Collectors.joining(","));
this.sqlBuilder.append(condition).append(" ( ").append(s1).append(" ) ");
}
return this;
}
/**
* 追加倒叙排序条件
*
* @param field 排序字段
* @author 不可食用盐
*/
public DynamicQueryUtil appendOrderByDesc(String field) {
if (StrUtil.isNotBlank(field)) {
this.sqlBuilder.append(" ORDER BY ").append(field).append(" DESC ");
}
return this;
}
/**
* 追加升序排序条件
*
* @param field 排序字段
* @author 不可食用盐
*/
public DynamicQueryUtil appendOrderByAsc(String field) {
if (StrUtil.isNotBlank(field)) {
this.sqlBuilder.append(" ORDER BY ").append(field).append(" ASC ");
}
return this;
}
定义查询方法
这里基于jdbcTemplate
定义了封装了查询单个对象
、查询一组对象
和分页查询
的方法。其中查询单个对象
和查询一组对象
的方法比较简单,就是简单封了一层。
分页查询
的核心有两方面,一是根据分页查询参数Pageable
对象计算出limit
的初始位置和记录数,另一个是改写sql
使用count()
统计出执行sql
返回的总数据条数。
/**
* 返回单个对象
*
* @param jdbcTemplate
* @param tClass
* @return T
* @author 不可食用盐
*/
public <T> T singleForObject(JdbcTemplate jdbcTemplate, Class<T> tClass) {
printSql();
return jdbcTemplate.queryForObject(this.sqlBuilder.toString(), tClass);
}
/**
* 返回单个对象
*
* @param jdbcTemplate
* @param args
* @param tClass
* @return T
* @author 不可食用盐
*/
public <T> T singleForObject(JdbcTemplate jdbcTemplate, Object[] args, Class<T> tClass) {
printSql();
return jdbcTemplate.queryForObject(this.sqlBuilder.toString(), tClass, args);
}
/**
* 返回单个对象
*
* @param jdbcTemplate
* @param args
* @param argTypes
* @param tClass
* @return T
* @author 不可食用盐
*/
public <T> T singleForObject(JdbcTemplate jdbcTemplate, Object[] args, int[] argTypes, Class<T> tClass) {
printSql();
return jdbcTemplate.queryForObject(this.sqlBuilder.toString(), args, argTypes, tClass);
}
/**
* 返回一个map
*
* @param jdbcTemplate
* @return java.util.Map<java.lang.String, java.lang.Object>
* @author 不可食用盐
*/
public Map<String, Object> singleForMap(JdbcTemplate jdbcTemplate) {
printSql();
return jdbcTemplate.queryForMap(this.sqlBuilder.toString());
}
/**
* 返回一个map
*
* @param jdbcTemplate
* @param args
* @return java.util.Map<java.lang.String, java.lang.Object>
* @author 不可食用盐
*/
public Map<String, Object> singleForMap(JdbcTemplate jdbcTemplate, Object[] args) {
printSql();
return jdbcTemplate.queryForMap(this.sqlBuilder.toString(), args);
}
/**
* 返回一个map
*
* @param jdbcTemplate
* @param args
* @param argTypes
* @return java.util.Map<java.lang.String, java.lang.Object>
* @author 不可食用盐
*/
public Map<String, Object> singleForMap(JdbcTemplate jdbcTemplate, Object[] args, int[] argTypes) {
printSql();
return jdbcTemplate.queryForMap(this.sqlBuilder.toString(), args, argTypes);
}
/**
* 返回集合对象
*
* @param jdbcTemplate
* @param tClass 查询结果对象类型
* @return java.util.List<T>
* @author 不可食用盐
*/
public <T> List<T> list(JdbcTemplate jdbcTemplate, Class<T> tClass) {
printSql();
return jdbcTemplate.query(this.sqlBuilder.toString(), new BeanPropertyRowMapper<>(tClass));
}
/**
* 返回集合对象
*
* @param jdbcTemplate
* @param args 参数数组
* @param tClass 查询结果对象类型
* @return java.util.List<T>
* @author 不可食用盐
*/
public <T> List<T> list(JdbcTemplate jdbcTemplate, Object[] args, Class<T> tClass) {
printSql();
return jdbcTemplate.query(this.sqlBuilder.toString(), new BeanPropertyRowMapper<>(tClass), args);
}
/**
* 返回分页对象
*
* @param jdbcTemplate
* @param args 参数数组
* @param pageable 分页参数
* @param tClass 查询结果对象类型
* @return org.springframework.data.domain.Page<T>
* @author 不可食用盐
*/
public <T> Page<T> page(JdbcTemplate jdbcTemplate, Object[] args, Pageable pageable, Class<T> tClass) {
String sql = this.sqlBuilder.toString();
String countSql = getQueryResultTotalSql(sql);
int totalSize = jdbcTemplate.queryForObject(countSql, Integer.class);
if (totalSize == 0) {
return new PageImpl<>(new ArrayList<>(), pageable, totalSize);
}
String sortStr = "";
Sort sort = pageable.getSort();
if (sort.isSorted()) {
sortStr = sort.stream()
.map(order -> order.getProperty() + " " + order.getDirection())
.collect(Collectors.joining(" "));
}
if (StrUtil.isNotBlank(sortStr)){
int order = sql.toLowerCase().lastIndexOf("order");
if (order >= 0){
sql = sql.substring(order) + "ORDER BY " + sortStr;
}
}
int offset = (pageable.getPageNumber()) * pageable.getPageSize();
int limit = pageable.getPageSize();
sql = sql + " limit " + limit + " offset " + offset;
printSql();
List<T> content = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(tClass), args);
return new PageImpl<>(content, pageable, totalSize);
}
/**
* 返回分页对象
*
* @param jdbcTemplate
* @param pageable 分页参数
* @param tClass 查询结果对象类型
* @return org.springframework.data.domain.Page<T>
* @author 不可食用盐
*/
public <T> Page<T> page(JdbcTemplate jdbcTemplate, Pageable pageable, Class<T> tClass) {
String sql = this.sqlBuilder.toString();
String countSql = getQueryResultTotalSql(sql);
int totalSize = jdbcTemplate.queryForObject(countSql, Integer.class);
if (totalSize == 0) {
return new PageImpl<>(new ArrayList<>(), pageable, totalSize);
}
int offset = (pageable.getPageNumber()) * pageable.getPageSize();
int limit = pageable.getPageSize();
sql = sql + " limit " + limit + " offset " + offset;
printSql();
List<T> content = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(tClass));
return new PageImpl<>(content, pageable, totalSize);
}
这里没有让
DynamicQueryUtil
类成为Spring 的 Bean
所有采用了JdbcTemplate
作为参数传入方法
获取查询总结果数的sql
创建getQueryResultTotalSql
方法,这个方法的作用就是根据查询sql
创建一个能统计查询结果总数的sql
,就是将Select
和From
之间的查询内容换成Count(1)
。
思路:由于Select
和From
是成对出现的,所以只需要找到和首次出现的Select
对应的From
的位置,并将其位置之前的字符串替换成SELECT COUNT(1)
即可。
做法:先将sql
按空格分隔,遍历分隔后的字符数组,遇见Select
关键字就将当前位置入栈,遇见From
关键字就出栈。当栈为空时,说明这个位置之前的内容就是要被替换成SELECT COUNT(1)
的内容。
/**
* 获取统计总数的sql
*
* @param sql
* @return java.lang.String
* @author 不可食用盐
*/
private String getSqlResultCount(String sql) {
List<String> strList = Arrays.stream(sql.split(" "))
.map(String::trim)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toList());
// 创建一个栈,用于记录遍历过程中未匹配的selete
Stack<Integer> records = new Stack<>();
records.push(0);
long endIndex = 0;
for (int i = 1; i < strList.size(); i++) {
String str = strList.get(i);
if ("from".equalsIgnoreCase(str)) {
records.pop();
if (records.empty()) {
endIndex = i;
break;
}
}
if ("select".equalsIgnoreCase(str)) {
records.push(i);
continue;
}
}
String result = strList.stream()
.skip(endIndex)
.collect(Collectors.joining(" "));
result = "SELECT COUNT(1) " + result;
return result;
}
这里有个问题就是字符和
From
之间要有空格,如果字符和From
关键字之间没有空格会导致替换错误
本来是想使用正则来实现的,但是无奈水平有限。在网上找到了一个正则表达式(?<=(SELECT|select|Select)).*?(?=(FROM|from|From))
,结果碰见包含子查询的时候出现问题了,无奈只能弃用。
定义打印sql的方法
最后定义个打印sql
的方法,以便排除问题时知道执行的sql
。
/**
* 打印sql
* 只在dev或test环境下打印
*
* @return void
* @author 不可食用盐
*/
private void printSql() {
try {
String active = SpringUtil.getActiveProfile();
if ("dev".equals(active) || "test".equals(active)) {
log.info("待执行sql: {}", this.sqlBuilder.toString());
}
} catch (Exception exception) {
log.error("获取系统环境失败", exception);
}
}
使用示例
public void query(String name, Integer age){
String sql = "select * from user where del = false";
Page<User> userPage = DynamicQueryUtil.of(sql)
.dynamicAppendCondition("name = ", name)
.dynamicAppendCondition("age >=", age)
.appendOrderByDesc("create_time")
.page(jdbcTemplate, Pageable.ofSize(10), User.class);
}
才疏学浅水平有限,大佬们有更好的方法欢迎留言。