阅读 142

JDBC 查询日志记录变得更容易的方案汇总

在大多数情况下,JDBCPreparedStatement使执行数据库查询变得更加容易,并且可以显著提高你的整体应用程序性能。但是PreparedStatement当涉及到记录查询语句时,该接口就不够用了。尽管 aPreparedStatement的优势在于其可变性,但一个好的日志条目必须准确描述发送到数据库的 SQL 在所有参数占位符已被实际参数值替换后的外观。尽管有多种方法可以解决这个难题,但没有一种方法可以轻松大规模实现,而且大多数方法都会使你的代码变得混乱。

在本文中,你将学习如何扩展 JDBCPreparedStatement接口以进行查询日志记录。虽然LoggableStatement类实现的PreparedStatement接口,但增加了在适合于记录的格式获得查询字符串的方法。使用LoggableStatement该类既可以减少日志代码中的错误发生率,又可以随着时间的推移生成更整洁、更易于管理的代码。

请注意,本文假设你之前有使用 JDBC 和PreparedStatement类的经验。

典型的查询记录解决方案

伪原创工具 https://www.237it.com/ 

清单 1 说明了PreparedStatement在进行数据库查询时通常如何使用 a(尽管省略了初始化和错误处理)。我们将使用SQL查询SELECT我们的例子在这篇文章中,但讨论也同样适用于其他类型的SQL语句如DELETE,UPDATE和INSERT。

清单 1. 一个典型的 SQL 数据库查询
String sql = "select foo, bar from foobar where foo < ? and bar = ?";     String fooValue = new Long(99);     String barValue = "christmas";     Connection conn = dataSource.getConnection();     PreparedStatement pstmt = conn.prepareStatement(sql);     pstmt.setLong(1,fooValue);     pstmt.setString(2,barValue);     ResultSet rs = pstmt.executeQuery();     // parse result...

清单 1 中查询的良好日志条目可能如下所示:

Executing query: select foo,bar from foobar where foo < 99 and bar='christmas'

下面是该条目的日志记录代码外观的一个示例。请注意,清单 1 中的问号已替换为每个参数的值。

System.out.println("Executing query: select foo, bar from foobar where foo < "+fooValue+" and bar = '+barValue+"'")

更好的方法是创建一个方法,我们称之为replaceFirstQuestionMark,它接受查询字符串并用参数值替换问号,如清单 2 所示。使用这种方法消除了创建重复字符串来描述的需要SQL 语句。

清单 2. 使用 replaceFirstQuestionMark 进行字符串替换
// listing 1 goes here      sql = replaceFirstQuestionMark(sql, fooValue);      sql = replaceFirstQuestionMark(sql, barValue);      System.out.println("Executing query: "+sql);

虽然易于实施,但这些解决方案都不是理想的。问题是,每当对 SQL 模板进行更改时,日志记录代码也必须更改。在某些时候你会犯错误几乎是不可避免的。查询将被更改,但您将忘记更新日志记录代码,并且你最终会得到与发送到数据库的查询不匹配的日志条目——这是调试噩梦。

我们真正需要的是一个设计,让我们使用的每个参数变量(fooValuebarValue在我们的例子)只有一次。我们想要一种方法,它可以让我们获取参数占位符替换为实际值的查询字符串。因为java.sql.PreparedStatement没有这样的方法,我们必须自己实现一个。

自定义解决方案

我们的自定义实现PreparedStatement将充当 JDBC 驱动程序提供的“真实语句”的包装器。包装器语句会将所有方法调用(例如,setLong(int, long)setString(int,String))转发到“真实语句”。在这样做之前,它将保存相关的参数值,以便它们可用于生成日志输出。

清单 3 显示了LoggableStatement类是如何实现的java.sql.PreparedStatement,以及它是如何使用 JDBC 连接和 SQL 模板作为输入来构建的。

清单 3. LoggableStatement 实现 java.sql.PreparedStatement
public class LoggableStatement implements java.sql.PreparedStatement {      // used for storing parameter values needed       // for producing log      private ArrayList parameterValues;      // the query string with question marks as      // parameter placeholders      private String sqlTemplate;      // a statement created from a real database      // connection      private PreparedStatement wrappedStatement;     public LoggableStatement(Connection connection, String sql)       throws SQLException {       // use connection to make a prepared statement       wrappedStatement = connection.prepareStatement(sql);       sqlTemplate = sql;       parameterValues = new ArrayList();     }      }

LoggableStatement 如何工作

清单 4 说明了LoggableStatement如何添加对saveQueryParamValue()方法的调用,以及如何在方法setLong和 setStringreal语句上调用相应的方法。saveQueryParamValue()调用以类似的方式添加到用于参数设置的所有方法(例如setCharsetLongsetRef、 和setObj)。清单 4 还展示了在不调用saveQueryParamValue()的情况下如何包装方法executeQuery,因为它不是“参数设置”方法。

清单 4. LoggableStatement 方法
public void setLong(int parameterIndex, long x)          throws java.sql.SQLException {       wrappedStatement.setLong(parameterIndex, x);       saveQueryParamValue(parameterIndex, new Long(x));    }    public void setString(int parameterIndex, String x)        throws java.sql.SQLException {       wrappedStatement.setString(parameterIndex, x);       saveQueryParamValue(parameterIndex, x);    }   public ResultSet executeQuery() throws java.sql.SQLException {      return wrappedStatement.executeQuery();    }

saveQueryParamValue()方法如清单 5 所示。它将每个参数值转换为一种String表示形式,并将其保存以供该getQueryString方法以后使用。默认情况下,对象将String使用其toString方法转换为 a ,但如果对象是 aStringa Date,它将用单引号 (") 括起来。getQueryString()方法允许你从日志中复制大多数查询并将它们粘贴到交互式 SQL 处理器中进行测试和调试,而无需修改。你可以根据需要修改该方法以转换其他类的参数值。

清单 5. saveQueryParamValue() 方法
private void saveQueryParamValue(int position, Object obj) {       String strValue;       if (obj instanceof String || obj instanceof Date) {            // if we have a String, include '' in the saved value            strValue = "'" + obj + "'";       } else {            if (obj == null) {                 // convert null to the string null                  strValue = "null";            } else {                 // unknown object (includes all Numbers), just call toString                 strValue = obj.toString();            }       }       // if we are setting a position larger than current size of       // parameterValues, first make it larger       while (position >= parameterValues.size()) {            parameterValues.add(null);       }       // save the parameter       parameterValues.set(position, strValue);  }

当使用标准方法设置所有参数时,我们只需调用我们的getQueryString()方法LoggableStatement来获取查询字符串。所有问号都将替换为实际参数值,这些值已准备好输出到我们选择的日志记录目的地。

使用 LoggableStatement

清单 6 显示了如何将清单 1 和 2 中的代码更改为使用LoggableStatement。在我们的应用程序代码引入LoggableStatement解决了重复参数变量的问题。当对SQL模板进行更改时,我们只需要更新PreparedStatement参数设置调用(例如,添加pstmt.setString(3,"new-param-value"))。更改将反映在日志输出中,无需对日志代码进行任何手动更新。

清单 6. 工作中的 LoggableStatement
String sql = "select foo, bar from foobar where foo < ? and bar = ?";     long fooValue = 99;     String barValue = "christmas";     Connection conn = dataSource.getConnection();     PreparedStatement pstmt;     if(logEnabled) // use a switch to toggle logging.         pstmt = new LoggableStatement(conn,sql);     else         pstmt = conn.prepareStatement(sql);     pstmt.setLong(1,fooValue);     pstmt.setString(2,barValue);     if(logEnabled)        System.out.println("Executing query: "+          ((LoggableStatement)pstmt).getQueryString());     ResultSet rs = pstmt.executeQuery();

结论

通过本文中概述的简单步骤,你可以扩展用于查询日志记录的 JDBCPreparedStatement接口。我们在这里使用的技术可以被认为是“包装扩展”或装饰器设计模式的一个实例。在必须扩展 API 但不能选择子类化的情况下,通过包装进行扩展非常有用。


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐