装饰器模式( Decorator Pattern): Oracle 数据库打印PreparedStatement

本文参考:

  1. Overpower PreparedStatement
  2. Bind variables - The key to application performance

设计模式用前须知

  • 设计模式中一句出现频率非常高的话是,“ 在不改动。。。。的情况下, 实现。。。。的扩展“ 。
  • 对于设计模式的学习者来说,充分思考这句话其实非常重要, 因为这句往往只对框架/ 工具包的设计才有真正的意义。因为框架和工具包存在的意义,就是为了让其他的程序员予以利用, 进行功能的扩展,而这种功能的扩展必须以不需要改动框架和工具包中代码为前提
  • 对于应用程序的编写者, 从理论上来说, 所有的应用层级代码至少都是处于可编辑范围内的, 如果不细加考量, 就盲目使用较为复杂的设计模式, 反而会得不偿失, 毕竟灵活性的获得, 也是有代价的。

PrepareStatement 的好处

有一定java数据库开发经验的人都知道PreparedStatement 相对于Statement的优点 :

  • 因为预解析(Parse),编译(Compile)了SQL语句, 计划(Plan)了SQL的数据获取路径, 所以通常来说PreparedStatement都会比Statement快, 至少不会比Statement慢
  • 防止了SQL 注入, 因为它对可执行SQL和数据进行了分离 , 数据参数会被在SQL语句转化为执行计划以后, 另作单独的数据包传输过去,所以避免了拼接可能引发的SQL注入问题
  • 简化了设置非标准类型参数的方法,例如 Date, Time, Timestamp, BigDecimal, InputStream (Blob) , Reader (Clob)
    • 例如
preparedStatement = connection.prepareStatement("INSERT INTO Person (name, email, birthdate, photo) VALUES (?, ?, ?, ?)");
preparedStatement.setString(1, person.getName());
preparedStatement.setString(2, person.getEmail());
preparedStatement.setTimestamp(3, new Timestamp(person.getBirthdate().getTime()));
preparedStatement.executeUpdate();

如何打印PrepareStatement

但是使用PreparedStatement 对于需要保留sql 语句执行记录的场景可能会遇到问题( 例如本人所在的公司交易系统应用对所有的sql 语句都会记log ), 如果想打印以上的PreparedStatement 真正被执行的sql 语句,获得了如下形式的内容

例如 INSERT INTO Person (name, email, birthdate) VALUES (John, john@hotmail.com,19660606)

则有可能遇到问题。 因为JDBC API 并没有定义专门用于获取PreparedStatement 执行语句内容的方法, 而PreparedStatemet.toString() 获得的结果取决于使用JDBC Driver, 如果使用的是PostgreSQL 8.x and MySQL 5.x, 那直接在参数被set 后,调用 System.out.println(preparedStatement); 即可获得想要sql 语句内容。 如果是其他的JDBC Driver 例如 Oracle , 执行该语句只会获得一串对象码。

所以问题情境是,当前开发的系统中,使用了PreparedStatement 的地方都没有办法打印带参数的sql 语句, 只能打印出带问号的部分, 在应用测试阶段不便于问题的排查, 所以需要一种最低侵入式, 改动范围最小的方法支持PreparedStatement 语句的打印 。

  • 可能的解决思路:
    • 继承 oracle jdbc 对 PreparedStatement实现类, 重写set 方法, 在set被调用时记录参数内容, 然后自行添加toString 方法, 拼接处整的sql 语句内容
      • 事实证明不可行, 因为ojdbc driver 的 PreparedStatement 的实现类的访问权限是包内权限, 所以无法继承。
    • 使用Log4jdbc 或 P6Spy 等来自StackOverFlow 推荐的日志工具
      • 缺点: 为了一个小需求给项目添加了新的依赖, 还需要一系列设置, 太麻烦。
    • 利用设计模式之装饰器模式(Decorator), 实现一个DebuggableStatement接口, 以下列方式打印PreparedStatement
      • 缺点: 除需要向原有的项目中添加几个类以外, 几乎没有。
Connection con = DriverManager.getConnection(url);

// 当DebugLevel 为OFF时, StatementFactory.getStatement(con,sql,debug);返回的依旧是PreparedStatement 
DebugLevel debug = DebugLevel.ON;
String sql = "SELECT name,rate FROM EmployeeTable WHERE rate > ?";

//下面通过一个工厂类而不是Connection来获取PreparedStatement, 
//PreparedStatement ps = con.prepareStatement(sql);
  PreparedStatement ps = StatementFactory.getStatement(con,sql,debug);
  ps.setInt(1,25);
  
//如果 ps 是一个实现了DebuggableStatement的对象, 便可以打印出实际执行的sql语句
//otherwise, an object identifier is displayed
  System.out.println(" debuggable statement= " + ps.toString());
  • 优点:

    • 对原有业务部分的代码改动很小, 只需要修改一行获取PreparedStatement 的方式 。
    • 不需要添加额外的依赖库
    • 可以通过一个变量开关决定使用的PreparedStatement 类型, 方便随时关闭DebuggableStatement 的开销
  • 代码实现方式(代码来自参考文章一, 但是做了一些改动, 避免报错无法运行):

    • 需要向项目中新增以下类:
      • DebuggableStatement
      • StatementFactory
      • SqlFormatter
      • DefaultSqlFormatter
      • OracleSqlFormatter
      • DebugLevel
  • 代码说明:

    • DebuggableStatement 是这里的核心, 也是应用代理模式的地方
      • 需要注意的是PreparedStatement 是一个接口, 由数据库提供商的Driver 提供商提供实现, 所以这里DebuggableStatement要实现PreparedStatement 接口, 需要实现一大堆方法, 很显然,这个让我们自己实现几乎不可行, 所以这里便是应用装饰器模式的绝佳场景 。 下面抽取DebuggableStatement 的部分代码进行说明。
 class DebuggableStatement implements PreparedStatement{

    private PreparedStatement ps;       //preparedStatement being proxied for.
    private String sql;                 //original statement going to database.
    private String filteredSql;         //statement filtered for rogue '?' that are not bind variables.
    private DebugObject[] variables;    //array of bind variables
    private SqlFormatter formatter;     //format for dates
    private long startTime;             //time that statement began execution
    private long executeTime;           //time elapsed while executing statement
    private DebugLevel debugLevel;      //level of debug

	....

	@Override
    public boolean getMoreResults(int current) throws SQLException {
        return ps.getMoreResults();
    ....
    }

注意到DebuggableStatement第一个成员对象ps 是PreparedStatement , 而我们实现PreparedStatement 的方法内容都无须自行编写, 都是直接调用ps对象的对应方法。 而ps 对象的获取是在如下的构造方法中, 通过connection 获取。

protected DebuggableStatement(Connection con, String sqlStatement, SqlFormatter formatter, DebugLevel debugLevel) throws SQLException{
        //set values for member variables
        if (con == null)
            throw new SQLException("Connection object is null");
        this.ps = con.prepareStatement(sqlStatement); //被代理对象的获取
        this.sql = sqlStatement;
        this.debugLevel = debugLevel;
        this.formatter = formatter;

至此, DebuggableStatement 的实现方式就已经完全清晰了, 至于sql 的拼接就是在每一个set 中将具体的参数值保存在一个数组中, 最后toString 的时候, 填充即可

  • SqlFormatter
  • OracleSqlFormatter

这两个类只是为了实现sql 语句中, 对于日期(Date)等非基本类型的参数打印的格式化实现而已,非常简单。

完整的代码


  • DebuggableStatement
 class DebuggableStatement implements PreparedStatement{

    private PreparedStatement ps;       //preparedStatement being proxied for.
    private String sql;                 //original statement going to database.
    private String filteredSql;         //statement filtered for rogue '?' that are not bind variables.
    private DebugObject[] variables;    //array of bind variables
    private SqlFormatter formatter;     //format for dates
    private long startTime;             //time that statement began execution
    private long executeTime;           //time elapsed while executing statement
    private DebugLevel debugLevel;      //level of debug

    /**
        Construct new DebugableStatement.
        Uses the SqlFormatter to format date, time, timestamp outputs
        @param con Connection to be used to construct PreparedStatement
        @param sqlStatement sql statement to be sent to database.
        @param debugLevel DebugLevel can be ON, OFF, VERBOSE.

    */
    protected DebuggableStatement(Connection con, String sqlStatement, SqlFormatter formatter, DebugLevel debugLevel) throws SQLException{
        //set values for member variables
        if (con == null)
            throw new SQLException("Connection object is null");
        this.ps = con.prepareStatement(sqlStatement);
        this.sql = sqlStatement;
        this.debugLevel = debugLevel;
        this.formatter = formatter;

        //see if there are any '?' in the statement that are not bind variables
        //and filter them out.
        boolean isString = false;
        char[] sqlString = sqlStatement.toCharArray();
        for (int i = 0; i < sqlString.length; i++){
            if (sqlString[i] == '\'')  // 为了判断要替换的问号是否是否在单引号内部
                isString = !isString;
            //substitute the ? with an unprintable character if the ? is in a
            //string.
            if (sqlString[i] == '?' && isString) // 单引号中的问号不能替换
                sqlString[i] = '\u0007';
        }
        filteredSql = new String(sqlString);

        //find out how many variables are present in statement.
        int count = 0;
        int index = -1;
        while ((index = filteredSql.indexOf("?",index+1)) != -1){
            count++;
        }

        //show how many bind variables found
        if (debugLevel == DebugLevel.VERBOSE)
            System.out.println("count= " + count);

        //create array for bind variables
        variables = new DebugObject[count];

    }

    /**
     * Facade for PreparedStatement
     */
    public void addBatch() throws SQLException{
        ps.addBatch();
    }

    /**
     * Facade for PreparedStatement
     */
    public void addBatch(String sql) throws SQLException{
        ps.addBatch();
    }

    /**
     * Facade for PreparedStatement
     */
    public void cancel() throws SQLException{
        ps.cancel();
    }

    /**
     * Facade for PreparedStatement
     */
    public void clearBatch() throws SQLException{
        ps.clearBatch();
    }

    /**
     * Facade for PreparedStatement
     */
    public void clearParameters() throws SQLException{
        ps.clearParameters();
    }

    /**
     * Facade for PreparedStatement
     */
    public void clearWarnings() throws SQLException{
        ps.clearWarnings();
    }

    /**
     * Facade for PreparedStatement
     */
    public void close() throws SQLException{
        ps.close();
    }

    /**
     * Executes query and Calculates query execution time if DebugLevel = VERBOSE
     * @return results of query
     */
    public boolean execute() throws SQLException{
        //execute query
        Boolean results = null;
        try{
            results = (Boolean)executeVerboseQuery("execute",null);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值