IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  Java technology  >

JDBC 查询日志变得简单

使用增强型PreparedStatement向JDBC代码增加日志功能

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

Jens Wyke (jens.wyke@se.ibm.com), 顾问, IBM 

2003 年 7 月 17 日

JDBC java.sql.PreparedStatement接口的简单扩展可以使查询记录更少犯错,同时整理您的代码。在本文中,IBM电子商务顾问Jens Wyke向您介绍如何应用基本的封装技术(“通过封装来实现扩展”也称为Decorator设计模式)来获得最满意的结果。

在大多数情况下,JDBC PreparedStatements 使执行数据库查询更简便并可以显著提升您整体应用程序的性能。当谈到日志查询语句时 PreparedStatement 接口就显得有些不足了。 PreparedStatement 的优势在于其可变性,但是一个好的日志条目必须正确描述如何将SQL发送到数据库,它将密切关注用实际的参数值来替换所有参数占位符。虽然有多种方法可以解决这一难题,但没有任何一种易于大规模实施并且大部分将扰乱您的程序代码。

在本文中,您将了解到如何扩展JDBC PreparedStatement 接口来进行查询日志。 LoggableStatement 类实现 PreparedStatement 接口,但添加用于获得查询字符串的方法,使用一种适用于记录的格式。使用 LoggableStatement 类可以减少日志代码中发生错误的几率,生成简单且易于管理的代码。

注意:本文假设您有丰富的JDBC和 PreparedStatement 类经验。

典型日志解决方案

表1介绍了数据库查询时通常是如何使用 PreparedStatement (虽然忽略了初始化和错误处理)。在本文中,我们将使用SQL query SELECT 做为例子,但讨论使用其它类型的SQL语句,如 DELETEUPDATEINSERT


表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驱动器提供的“真实语句(real statement)”的封装器(Wrapper)。封装器语句将转发所有方法调用(例如 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() 方法添加一个调用,以及在方法 setLongsetString 的“真实语句”上调用相应的方法。我们采用与用于参数设置的所有方法(例如 setCharsetLongsetRefsetObj )相同的方式来增加 saveQueryParamValue() 调用。表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();
   }

表5中显示了 saveQueryParamValue() 方法。它把每个参数值转换成 String 表示,保存以便 getQueryString 方法日后使用。缺省情况下,一个对象使用其 toString 方法将被转换成 String ,但如果对象是 StringDate ,它将用单引号('')表示。 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);
 }

当我们使用标准方法来设置所有参数时,我们在 LoggableStatement 中简单调用 getQueryString() 方法来获得查询字符串。所有问号都将被真正的参数值替换,它准备输出到我们选定的日志目的地。





回页首


使用LoggableStatement

表6显示如何更改表1和表2中的代码来使用 LoggableStatement 。将 LoggableStatement 引入到我们的应用程序代码中可以解决复制的参数变量问题。如果改变了SQL模板,我们只需更新 PreparedStatement 上的参数设置调用(例如添加一个 pstmt.setString(3,"new-param-value") )。这一更改将在日志输出结果中反映出,无需任何记录代码的手工更新。


表6:使用LoggableStatement&#160
    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();





回页首


结束语

使用本文介绍的非常简单的步骤,您可以为查询记录扩展JDBC PreparedStatement 接口。我们在此处使用的技术可以被视为“通过封装来实现扩展”,或作为Decorator设计模式的一个实例(见 参考资料)。通过封装来实现扩展在当您必须扩展API但subclassing不是一项可选功能时极其有用。

您将在 参考资料部分找到 LoggableStatement 类的源代码。您可以按原样使用它,或者进行定制以满足您的数据库应用程序的特殊需求。



参考资料



关于作者

Jens Wyke 是瑞典IBM企业咨询服务部顾问,他设计和构建了电子商务解决方案并经常使用Java技术和IBM WebSphere系列产品。可以通过 jens.wyke@se.ibm.com与 Jens联系。




对本文的评价










回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款