「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战」
前言
上一篇文章中"only_full_group_by"sql模式对GROUP BY的影响及处理,我对这个问题的处理想法是写一个拦截器,对于包含group by的sql进行拦截并加上any_value(),但是后来一想每次都要拦截修改还不如直接修改一次效率更高。同时我也在尝试了其他的方式,最终选择了给select列表添加any_value(),含有group by的sql单独使用一个公用的select列表,下面是我在解决这个问题的记录。
使用mybits拦截器
上一篇文章中我自作聪明想到了使用mybits拦截器,于是我就开始去学习了mybits拦截器,由于第一次接触拦截器,踩了不少坑,这里顺便记录一下springboot+mybits如何使用mybits拦截器。
首先我的思路是在sql预编译的时候拦截,判断sql是否包含group by语句,如果包含就对select 列表判断和加any_value()。下面是我的代码(针对简单的查询,复杂查询子查询还没写到就弃用这个方式):
//前提是含有group by的sql才处理
//处理SELECT 语句的select列表,前提是
public StringBuilder modifySqlSelectList(String sql) {
StringBuilder sqlBuilder = new StringBuilder();
//去掉多余的空格
List<String> sqlToList = Arrays.stream(
sql.replace("\n", " ")
.replace("\t", " ")
.split(" "))
.filter(s -> !"".equals(s.trim()))
.collect(Collectors.toList());
//使用空格分割
sqlToList.forEach(s -> {
if("select".equalsIgnoreCase(s) || "from".equalsIgnoreCase(s)){
sqlBuilder.append(s.toLowerCase(Locale.ROOT)).append(" ");
}else {
sqlBuilder.append(s).append(" ");
}
});
//取出select列表,group by列表
List<String> selectList;
List<String> selectListNew = new ArrayList<>();
String sqlSelect = sqlBuilder.toString();
int s1 = sqlSelect.indexOf("select") + 6;
int s2 = sqlSelect.indexOf("from");
String selectLi = sqlSelect.substring(s1, s2);
//使用逗号分割select列表
String[] selectLiSplit = selectLi.split(",");
selectList = Arrays.asList(selectLiSplit);
//处理select列表,添加聚合函数 ANY_VALUE()
for (String s : selectList) {
if (s.trim().contains(" ")) { //如果含有空格,说明有别名,不改变原别名
int ss = s.trim().indexOf(" ");
String col = s.trim().substring(0, ss);
if (!col.contains("(") && !col.contains(")")) { //不是一个聚合函数列
s = "ANY_VALUE(" + col + ") " + s.trim().substring(col.length()).trim();
}else { //是聚合列
// 这里有可能是 (id) as number 这种情况,应该判断括号前是否有字母
int s3 = col.indexOf("(");
// (id)
if("".equals(col.substring(0,s3).trim())){
s = "ANY_VALUE(" + col + ") " + s.trim().substring(col.length()).trim();
//ANY_VALUE((id)) as number 这样也没问题
}
}
} else {
//加上ANY_VALUE()
s = "ANY_VALUE(" + s.trim() + ") as " + s;
}
selectListNew.add(s.trim());
}
//拼接回原来的sql
StringBuilder sqlSelectNew = new StringBuilder();
selectListNew.forEach(s -> sqlSelectNew.append(s).append(", "));
//替换select列表最后一个列的逗号和逗号为空格
sqlSelectNew.replace(sqlSelectNew.length() - 2, sqlSelectNew.length(), " ");
//替换select列表
sqlBuilder.replace(s1 + 1, s2, sqlSelectNew.toString());
return sqlBuilder;
}
大概就是取出select到from之间的列表,分别加上any_value()再拼接回去,如果是聚合列就不处理,如果含有别名不能改变原来的别名。
在写完了简单的查询后,发现每次查询都要拦截处理,还不如直接修改mapper文件效率高,但是后来事实证明也没有白写,可以把需要改的列表复制过来直接生成,相当于写了个工具方法。
springboot+mybits+mybits拦截器
为了方便测试,我首先在我的demo上实现了拦截器,具体步骤如下:
新建GroupBySqlInterceptor 实现了org.apache.ibatis.plugin.Interceptor接口,
重写接口定义的方法:
@Log4j2
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
@Component
public class GroupBySqlInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Interceptor.super.plugin(target);
}
@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
}
**@Intercepts **注解用来指定拦截的对象和方法。
@Signature: type:拦截哪一类对象;
method:拦截什么方法;
args:方法参数类型;(**mybits3.4以上需要加上Integer.class**)
@Component: 使用这个注解能够实现bean的注入,所以不需要再编写配置文件。
主要要实现的方法是:intercept方法。
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(), new DefaultReflectorFactory());
MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
//sql
BoundSql sql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
//sql类型
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
//在这对sql修改
return invocation.proceed();
}
有趣的查询
在观察了sql之后,我发现大多数sql包含group by的都是 group by 一个字段。目的是根据这个字段去重,比如满足条件的有多行,但是broup by后的字段有重复的,就只需要其中一条就行。
根据这个我又想能不能通过子查询实现去重。
首先我们的数据如下
我想要实现按照年龄分组并且要获取所有字段。
在only_full_group_by开启时,以下查询会报错
select id, name,age,salary from emp group by age;
于是我给select加上any_value()
SELECT
any_value ( id ), any_value ( name ), age, any_value ( salary )
FROM emp
GROUP BY age;
age在group by列,所以不用加
既然没有select 聚合函数列,那么使用子查询去重也可以。
SELECT
id ,
name,
age,
salary
FROM
emp
WHERE
id in (SELECT min(id) FROM emp GROUP BY age)
但是这样做做了两次查询,并且如果有查询条件的话父子查询都要携带相同的查询条件,否则min(id)不一定是我们想要的id。 (这里纠正一下,查询条件只需要子查询携带即可,因为子查询已经根据条件把数据查询出来了,使用min(id)拿到了数据的id,所以父查询不需要条件了)
发现一个有趣的查询,把min改成any_value的时候:
SELECT
id ,
name,
age,
salary
FROM
emp
WHERE
id in (SELECT any_value(id) FROM emp GROUP BY age)
我们把子查询拿出来单独查询:
SELECT any_value(id) FROM emp GROUP BY age;