Mybatis拦截器处理Geometry类型数据优化版

1,822 阅读4分钟

MySQL对一般数据类型支持的都很好,可是Geometry类型数据处理就比较麻烦。

前两天看到地理坐标类型的数据,其实不建议使用MySQL,一般用Redis或者MongoDB,还有PostgreSQL+PostGIS

不要看不起MySQL,其实你与Geometry之间只差一个拦截器。

之前写过一个拦截器,一开始的时候还觉得写的不错,现在回过头再来看,真的是一塌糊涂啊。

核心就是对SQL解析,找到占位符的位置,然后添加下面的函数。

  • 新增要使用SQL函数:ST_GEOMFROMTEXT()
  • 查询要使用SQL函数:ST_ASTEXT()

一开始试了一下查询没什么问题,后来使用的时候发现问题不少。

  1. 由于是解析SQL的占位符,然后记录下来位置,然后在对应位置添加函数,使用Mybatis-Plus自带的查询函数,偶尔会出问题,因为这个位置可能会不一致,有时候会变,几率很小。
  2. 自己写SQL,百分百会出问题。
  3. 增加和修改也支持的不好。

所以今天就来重新写一下,之前也是没有好的思路,所以用解析SQL字符串的方式,今天就来换一个方法。使用反射。

不过有一点得注意,就是数据库里的列的名称和Java对象的属性要对应,只能是使用下换线转驼峰的命名规则,否则还是不行。

import com.ler.manager.annotation.Geometry;
import com.ler.manager.util.CommonUtil;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.util.HashMap;
import java.util.List;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.RowBounds;

/**
 * @author lww
 * @date 2020-08-01 18:35
 */
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
@Slf4j
public class MybatisGeometryInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        //SELECT操作
        if (SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {
            List<ResultMap> resultMaps = mappedStatement.getResultMaps();
            ResultMap map = resultMaps.get(0);
            Class<?> type = map.getType();
            StringBuilder sb = new StringBuilder();
            sb.append(SqlCommandType.SELECT.name());
            String originalSql = boundSql.getSql();
            String reSql = originalSql.replace(SqlCommandType.SELECT.name(), "");
            int from = reSql.indexOf("FROM");
            Field[] declaredFields = type.getDeclaredFields();
            for (Field field : declaredFields) {
                field.setAccessible(true);
                Geometry annotation = field.getAnnotation(Geometry.class);
                String filedName = CommonUtil.humpToUnderline(field.getName());
                if ("serialVersionUID".equalsIgnoreCase(filedName)) {
                    continue;
                }
                if (annotation == null) {
                    sb.append(" ").append(filedName).append(" ,");
                } else {
                    sb.append("ST_ASTEXT(").append(filedName).append(") AS ").append(filedName).append(" ,");
                }
            }
            sb.deleteCharAt(sb.length() - 1);
            String lastSql = reSql.substring(from);
            sb.append(lastSql);
            metaObject.setValue("delegate.boundSql.sql", sb.toString());
            metaObject.setValue("delegate.rowBounds.offset", RowBounds.NO_ROW_OFFSET);
            metaObject.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT);
            return invocation.proceed();
        } else if (SqlCommandType.INSERT.equals(mappedStatement.getSqlCommandType())) {
            Object paramObj = boundSql.getParameterObject();
            Field[] fields;
            if (paramObj instanceof HashMap) {
                fields = ((HashMap) paramObj).get("et").getClass().getDeclaredFields();
            } else {
                fields = paramObj.getClass().getDeclaredFields();
            }
            String originalSql = boundSql.getSql();
            int start = originalSql.indexOf("(");
            int end = originalSql.indexOf(")");
            String paramNames = originalSql.substring(start + 1, end);
            String[] split = paramNames.split(",");

            originalSql = originalSql.toUpperCase();
            String substring = originalSql.substring(0, originalSql.indexOf("VALUES"));
            StringBuilder sb = new StringBuilder();
            sb.append(substring).append(" ").append("VALUES ").append("( ");
            for (String paramName : split) {
                for (Field field : fields) {
                    if (field.getName().trim().equalsIgnoreCase(CommonUtil.underlineToHump(paramName.trim()))) {
                        Geometry annotation = field.getAnnotation(Geometry.class);
                        if (annotation != null) {
                            sb.append("ST_GEOMFROMTEXT(").append(" ? ").append(")").append(" ,");
                        } else {
                            sb.append("?").append(" ,");
                        }
                    }
                }
            }
            sb.deleteCharAt(sb.length() - 1).append(" )");
            metaObject.setValue("delegate.boundSql.sql", sb.toString());
            return invocation.proceed();
        } else if (SqlCommandType.UPDATE.equals(mappedStatement.getSqlCommandType())) {
            StringBuilder sb = new StringBuilder();
            Object paramObj = boundSql.getParameterObject();
            Field[] fields;
            if (paramObj instanceof HashMap) {
                fields = ((HashMap) paramObj).get("et").getClass().getDeclaredFields();
            } else {
                fields = paramObj.getClass().getDeclaredFields();
            }
            String originalSql = boundSql.getSql();
            originalSql = originalSql.toUpperCase();
            int start = originalSql.indexOf("SET");
            int end = originalSql.indexOf("WHERE");
            sb.append(originalSql, 0, start + 4);
            String params = originalSql.substring(start + 4, end);
            String after = params.replace("=", "").replace("?", "");
            String[] split = after.split(",");
            for (String paramName : split) {
                for (Field field : fields) {
                    if (field.getName().trim().equalsIgnoreCase(CommonUtil.underlineToHump(paramName.trim()))) {
                        sb.append(paramName).append("=");
                        Geometry annotation = field.getAnnotation(Geometry.class);
                        if (annotation != null) {
                            sb.append("ST_GEOMFROMTEXT(").append(" ? ").append(")").append(" ,");
                        } else {
                            sb.append("?").append(" ,");
                        }
                    }
                }
            }
            sb.deleteCharAt(sb.length() - 1).append(originalSql.substring(end));
            metaObject.setValue("delegate.boundSql.sql", sb.toString());
            return invocation.proceed();
        }
        return invocation.proceed();
    }

    private Object realTarget(Object target) {
        if (Proxy.isProxyClass(target.getClass())) {
            MetaObject metaObject = SystemMetaObject.forObject(target);
            return realTarget(metaObject.getValue("h.target"));
        }
        return target;
    }

    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}

完整的拦截器代码就是上面这个了。

查询拦截

核心就是这个

for (Field field : declaredFields) {
    field.setAccessible(true);
    Geometry annotation = field.getAnnotation(Geometry.class);
    String filedName = CommonUtil.humpToUnderline(field.getName());
    if ("serialVersionUID".equalsIgnoreCase(filedName)) {
        continue;
    }
    if (annotation == null) {
        sb.append(" ").append(filedName).append(" ,");
    } else {
        sb.append("ST_ASTEXT(").append(filedName).append(") AS ").append(filedName).append(" ,");
    }
}

把类中的属性转为下划线命名,然后判断该属性上是否有注解Geometry,有的话就用函数包裹一下。

插入拦截

插入就比较麻烦一点。

if (paramObj instanceof HashMap) {
    fields = ((HashMap) paramObj).get("et").getClass().getDeclaredFields();
} else {
    fields = paramObj.getClass().getDeclaredFields();
}

第一个get("et"),是因为使用Mybatis-Plus自带的更新或者插入函数,他是把对象作为Map处理的,真正的参数是keyetvalue

如果自己写SQL就是对象。

核心是:

for (String paramName : split) {
    for (Field field : fields) {
        if (field.getName().trim().equalsIgnoreCase(CommonUtil.underlineToHump(paramName.trim()))) {
            Geometry annotation = field.getAnnotation(Geometry.class);
            if (annotation != null) {
                sb.append("ST_GEOMFROMTEXT(").append(" ? ").append(")").append(" ,");
            } else {
                sb.append("?").append(" ,");
            }
        }
    }
}

具体步骤是: 获取原SQL,获取到要注入的列,判断该列在Java对象中对应的属性上是否有注解,有的话就用函数包裹。

更新拦截

更新和插入差不多,不过一个是UPDATE,一个是INSERT

核心:

for (String paramName : split) {
    for (Field field : fields) {
        if (field.getName().trim().equalsIgnoreCase(CommonUtil.underlineToHump(paramName.trim()))) {
            sb.append(paramName).append("=");
            Geometry annotation = field.getAnnotation(Geometry.class);
            if (annotation != null) {
                sb.append("ST_GEOMFROMTEXT(").append(" ? ").append(")").append(" ,");
            } else {
                sb.append("?").append(" ,");
            }
        }
    }
}

核心代码都很相似,就多了一个=号。

最后删除就不用拦截了。

至此就完成了,来测试看一下效果

测试

  • 增加

  • 查询

  • 更新

可以看到,不论是使用Mybatis-Plus自带的方法还是自己写SQL,都能完美拦截,并且使用函数包裹。

用到的两个工具方法:

/**
* 驼峰转下划线
*/
public static String humpToUnderline(String str) {
String regex = "([A-Z])";
Matcher matcher = Pattern.compile(regex).matcher(str);
while (matcher.find()) {
    String target = matcher.group();
    str = str.replaceAll(target, "_" + target.toLowerCase());
}
return str;
}

/**
* 下划线转驼峰
*/
public static String underlineToHump(String str) {
String regex = "_(.)";
Matcher matcher = Pattern.compile(regex).matcher(str);
while (matcher.find()) {
    String target = matcher.group(1);
    str = str.replaceAll("_" + target, target.toUpperCase());
}
return str;
}

新的拦截器MybatisGeometryInterceptor就这样了,欢迎大家尝试。

一定要注意,数据库中的字段名称和Java类的属性要是下划线和驼峰的对应关系。

最后欢迎大家关注我的公众号

南诏Blog

共同学习,一起进步。加油🤣