一直以来动态查询都是
Jpa
的一个痛点。最近做的项目使用
Spring Data Jpa
作为持久层,每次碰见动态查询都很头疼,所以就想着封装一个工具用来处理动态查询。
定义一个工具类,在工具类中动态拼接
sql
条件,最后使用
JdbcTemplate
执行拼接好的
sql
。
定义工具类
定义一个类
DynamicQueryUtil
,并在
DynamicQueryUtil
类中定义一个
StringBuilder
类型的属性
sqlBuilder
。
sqlBuilder
属性用于拼接
sql
字符串。
* 动态查询工具
*
@author
不可食用盐
@Slf4j
public
class
DynamicQueryUtil
{
private
StringBuilder sqlBuilder;
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());
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);
才疏学浅水平有限,大佬们有更好的方法欢迎留言。