• Mybatis 获取最终可执行SQL语句


    一、问题提出

    从且只从一个PreparedStatement中获取执行的sql语句(包括运行时绑定的参数值),是实际工作中经常遇到的一个问题。

    网上很多文章提到用自定义的增强类或中间件(p6spy, log4jdbc)来实现,但这需要对现有代码进行修改,工作量很大,能不能有更直接的办法?

    由于java.sql.PreparedStatement并没有提供相应接口,此功能是否实现及如何实现,不同数据库的JDBC是不一样的。PostgreSQL和MySQL的DBC用toString接口实现了此功能;但对于Oracle,问题比较棘手。

    二、PostgreSQL和MySQL

    PostgreSQL和MySQL可以直接使用toString接口获取sql,且会得到绑定的参数值。两者的区别在于:MySQL会在前面加上类名。

    PreparedStatement ps = con.prepareStatement("SELECT value from sys_param where name=?");
    ps.setString(1, "UNIT_CODE");
    System.out.println(ps.toString());
    
    • 1
    • 2
    • 3

    PostgreSQL的输出是:SELECT value from sys_param where name=‘UNIT_CODE’,可以直接使用。

    MySQL的输出是:com.mysql.cj.jdbc.ClientPreparedStatement: SELECT value from sys_param where name=‘UNIT_CODE’,此时,只需将“: ”前部分截断即可。

    但Oracle的输出则是:oracle.jdbc.driver.OraclePreparedStatementWrapper@7b98f307,无法使用。

    三、Oracle

    经过仔细分析Oracle JDBC的类,发现oracle.jdbc.internal.OraclePreparedStatement(注意不能是oracle.jdbc.OraclePreparedStatement)提供了一个接口getOriginalSql()。用它进行尝试:

        PreparedStatement ps = con.prepareStatement("SELECT value from sys_param where name=?");
        ps.setString(1, "UNIT_CODE");
        if (ps instanceof OraclePreparedStatement) {
            OraclePreparedStatement ops = (OraclePreparedStatement)ps;
            System.out.println(ops.getOriginalSql());
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    输出是:SELECT value from sys_param where name=?,只差绑定的参数值了。

    那么,能不能进一步将绑定参数也解析出来?从目前掌握的信息看,Oracle还做不到。

    四、通用方法
    根据以上,可以构造一个从PreparedStatement获取sql的通用方法,如下:

    public String getSql(PreparedStatement ps) throws SQLException
    {
        if (ps==null || ps.getConnection()==null)
            return null;
    switch (ps.getConnection().getMetaData().getDatabaseProductName().toUpperCase())
        {
        case "ORACLE":
            OraclePreparedStatement ops = (OraclePreparedStatement)ps;
            return ops.getOriginalSql();
        case "MYSQL":
            String temp = ps.toString();
            return temp.substring(temp.indexOf(':') + 1);
        case "POSTGRESQL":
            return ps.toString();
        }
        return ps.toString();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    四、在Mybatis中的实现

    通过拦截器,拦截statementHandler,再通过上述逻辑获取SQL即可

    /**
     * 解析获取最终可执行SQL
     *
     * @author ZP.Lin
     */
    @Slf4j
    @Intercepts(
            @Signature(
                    type = StatementHandler.class,
                    method = "update",
                    args = Statement.class
            )
    )
    public class OperationSqlReportInterceptor implements Interceptor {
    
        /**
         * 获得真正的处理对象,可能多层代理.
         */
        public static <T> T realTarget(Object target) {
            if (Proxy.isProxyClass(target.getClass())) {
                MetaObject metaObject = SystemMetaObject.forObject(target);
                return realTarget(metaObject.getValue("h"));
            }
            return (T) target;
        }
    
        /**
         *  拦截StatementHandler.update方法后执行
         *  
         * @param invocation
         * @return
         * @throws Throwable
         */
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
    
            Statement s = (Statement) invocation.getArgs()[0];
            PreparedStatementLogger o = realTarget(s);
    
            Statement stmt = ((DruidPooledPreparedStatement) o.getPreparedStatement()).getStatement();
            PreparedStatement pstmt = null;
            // 配置druid连接时使用filters: stat配置
            if (stmt instanceof PreparedStatementProxyImpl) {
                pstmt = ((PreparedStatementProxyImpl) stmt).getRawObject();
            }
    
            String sql = RealSqlStrategyUtil.getSql(pstmt);
    
            log.error("statement 解析的SQL = {}", sql);
    
            return invocation.proceed();
        }
    
        @Override
        public Object plugin(Object target) {
            return Plugin.wrap(target, this);
        }
    
        @Override
        public void setProperties(Properties properties) {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    工具类

    /**
     * 获取可执行SQL策略工具
     *
     * @author ZP.Lin
     * @create 2022-07-26 16:09
     * @modified By
     **/
    @Slf4j
    public class RealSqlStrategyUtil {
    
        static Map<String, Function<PreparedStatement, String>> map = new HashMap<>();
    
        /*
          仿策略模式,方便后续扩展更多数据源.
         */
        static {
            map.put("ORACLE", ps -> {
                OraclePreparedStatement ops = (OraclePreparedStatement)ps;
                return ops.toString();
            });
            map.put("MYSQL", ps -> {
                String temp = ps.toString();
                return temp.substring(temp.indexOf(':') + 1);
            });
            map.put("POSTGRESQL", Object::toString);
        }
    
        /**
         * 获取最终可执行SQL
         *
         * @param ps
         * @return
         * @throws SQLException
         */
        public static String getSql(PreparedStatement ps) throws SQLException {
            if (ps == null || ps.getConnection() == null) {
                log.error("PreparedStatement is null ! ps = {}", ps);
                return null;
            }
            String dbType = ps.getConnection().getMetaData().getDatabaseProductName().toUpperCase();
            return getRealSqlByPreparedStatement(dbType, ps);
        }
    
        public static String getRealSqlByPreparedStatement(String dbType, PreparedStatement ps) {
            Function<PreparedStatement, String> statementStringFunction = map.get(dbType);
            if (statementStringFunction == null) {
                throw new RuntimeException("目前只支持 " + map.keySet() + " 数据源!");
            }
            return statementStringFunction.apply(ps).replaceAll("\\s+", " ");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    文章参考:http://www.cnblogs.com/wggj

  • 相关阅读:
    猿创征文|风起YuKon(禹贡)-空间数据库结缘数字孪生和实景三维建设
    Harbor----通过 Harbor 源码进行编译 Harbor
    【BOOST C++ 15 泛型编程】(1)Boost.TypeTraits
    使用funcgraph-retval和bpftrace/kprobe快速定位并解决cpu控制器无法使能的问题
    在基于ABP框架的前端项目Vue&Element项目中采用电子签名的处理
    C++【C++11】
    JS(二)数据类型,流程控制
    WPF文本绑定显示格式StringFormat设置-数值类型处理
    MVVM项目开发(商品管理系统三)
    python使用pandas中的read_csv函数读取csv数据为dataframe、筛选dataframe中的一个数据列的前几行数据
  • 原文地址:https://blog.csdn.net/m4330187/article/details/125996624