前言
在使用JDBC访问数据库的过程中,一般而言,有两种方式去执行SQL语句,使用java.sql.Statement,及使用java.sql.PreparedStatement,后者继承自前者,拥有前者的一切特征,但增加了预编译的功能,所以在任何时候都推荐使用该接口,尤其是在同一statement会被多次执行,而只是参数不同的情况下,可以得到极大的性能提升。
PreparedStatement的使用方法
使用PreparedStatement的时候,需要先准备一个包含未知参数的SQL语句,经过预编译后,再使用setInt, setString等方法来设置SQL语句中的参数的,其典型代码如下:
java.sql.Connection con = null;
java.sql.PreparedStatement pstmt = null;
String SQL="INSERT INTO CUSTOMER (ID, CHRCOMPANYID) VALUES ( ?, ?)";
con =……; //得到连接
pstmt = con.prepareStatement(SQL); //编译sql语句
pstmt.setInt(1, ID); //设置参数
pstmt.setString(2, companyID);
pstmt.executeUpdate(); 执行
}
|
这段代码比直接使用Statement的最大的优点就在于可以逐个设置参数,而不用构造一个非常复杂的SQL语句,java.sql.PreparedStatement中还有其它的方法如setDouble, setDate等方法用于设置其它类型的参数。因为参数的转换是由JDBC驱动来做的,这样就无须考虑各个数据库之间的不同,比如日期类型,只需要用setDate方法传递一个java.sql.Date的实例即可,而无须考虑使用什么样格式的字符串。这将使我们的程序更加具有可移植性。
PreparedStatement的缺点
如同前面所说,PreparedStatement的最大的优点就是可以动态设置参数,然后其缺点也来源于此,在执行SQL语句出错的时候,我们就没有办法看到完整的SQL语句,甚至也看不到设置的参数,这非常不便于调试。比如说,执行一个非常长的sql语句的时候,报错说插入的值超出范围,大部分JDBC驱动都不会报告是哪个字段出了问题,难道只能执行SQL语句之前将所有参数值手工打印出来吗?而程序调试通过后,这些垃圾代码又都需要清除掉。当一个程序处于不断的调试过程中,这将是极其痛苦的。
问题的分析与DebugPreparedStatement的实现
如何即保持PreparedStatement的优点,同时又便于调试呢?当然,任何时候,想要去掉调试信息的时候,都应该能够方便的去除它。可以想像,这仍然应当“是”一个PreparedStatement,因为我们需要保持原有的功能,只是这个新的类应该多做一些处理,以在出错的时候可以输出更详细的自定义的出错信息,比如参数列表。
是的,看过《设计模式》这本书的朋友应该想到了,这将是一个包装器(Wrapper)模式的应用。这里我不打算介绍该模式的特点及功用,本文的重点不是在此,对此感兴趣的朋友可以查看参考资料中的书目。
我将这个类命名为DebugPreparedStatement,实现了java.sql.PreparedStatement接口。根据包装器模式的结构特点(请参考《设计模式》4.4节),需要有一个指向另一个“真实的”PreparedStatement的指针。同时,由于我们需要记录下SQL语句中所用的参数,以便于出错时输入,所以我们需要一个集合来存放它们。这里我使用了TreeMap,因为它在内部排过序了。
看看类的声明部分吧。
import java.sql.*;
import com.beaconsystem.util.*;
/**
* 支持参数显示的PreparedStatement实现
* Creation time: (2001-6-16 0:48:09)
* @author: SonyMusic
*/
public class DebugPreparedStatement implements PreparedStatement {
//指向另一“真实”PreparedStatement
private PreparedStatement st=null;
//用于保存参数信息的Map
private java.util.Map parameterList=new java.util.TreeMap();
}
|
其次,我们需要在设置参数的时候,即setString及类似方法中,能记录下参数的位置和值,这也正是包装器模式的精华部分。请看部分实现:
/**
* Add debug message.
* Creation date: (6/14/2002 11:12:30 AM)
* @param param int
* @param obj java.lang.Object
*/
protected void addDebug(int param, Object obj) {
parameterList.put(new Integer(param), obj);
}
public void setInt(int parameterIndex, int x) throws SQLException {
addDebug(parameterIndex, new Integer(x));
//在将参数信息添加到Map中后,仍然执行该方法原有功能
st.setInt(parameterIndex,x);
}
public void setString(int parameterIndex, String x) throws SQLException {
addDebug(parameterIndex, x);
st.setString(parameterIndex,x);
}
|
还有其它各个setXXX方法,这里不再一一列出,但其实现都是类似的。
最后,在执行SQL语句的时候,如果出错,就在抛出意外的时候,同时将参数信息显示出来,于是我添加了一个“受保护的”方法getDebugMessage,该方法遍历保存参数信息的Map,并将其中的信息整理成String,再返回以供显示。在executeQuery等方法中,将会先调用内部st的相应方法,如果出错,则在原先有出错信息中加入参数信息,并重新构造一个新的java.sql.SQLException,并抛出。请看部分实现:
/**
* 得到全部的参数信息
* Creation date: (6/14/2002 11:00:50 AM)
* @return java.lang.String
*/
protected String getDebugMessage() {
StringBuffer buf=new StringBuffer(1000);
buf.append("Parameter Info:");
java.util.Iterator enu=parameterList.keySet().iterator();
while (enu.hasNext()) {
Integer key=(Integer)enu.next();
Object obj=parameterList.get(key);
buf.append("");
buf.append("Parameter ");
buf.append(key.intValue());
buf.append(":");
buf.append(obj);
if(obj instanceof java.lang.String){
buf.append("(Length:");
buf.append(((String)obj).length());
buf.append(")");
}
}
buf.append("");
return buf.toString();
}
public ResultSet executeQuery() throws SQLException {
try {
ResultSet ret = st.executeQuery();
return ret;
}
catch (SQLException e) {
throw new SQLException(e.getMessage()+getDebugMessage(), e.getSQLState());
}
}
|
其它如executeUpdate方法也与此实现类似,不再重复。
DebugPreparedStatement类中的其它方法,则是直接调用st的相应,不做任何处理。
DebugPreparedStatement的使用及优点
类已经写好了,下面将是如何使用的问题了。前面曾经提过,我们希望这个类是易于使用的,并且是易于拆卸的,只在我们需要调试信息的时候才使用,而一旦投入正式运行后,则不希望该类影响到运行速度,也不希望过多的垃圾代码影响程序的美观。
还是先来看看使用方法吧,请参考前面给出的PreparedStatement的使用
pstmt = con.prepareStatement(SQL); //编译sql语句
//重新包装了一次,但“看起来”仍然是一个PreparedStatement
pstmt=new DebugPreparedStatement(pstmt);
//其后的使用与原先无异
pstmt.setInt(1, ID); //设置参数
pstmt.setString(2, companyID);
pstmt.executeUpdate(); 执行
|
在出错的时候,我们也只需要简单的捕捉SQLException意外,并显示其getMessage(),就可以看到详细的参数信息了。
而在程序运行正确,不再需要调试信息的时候,只需要将新添加的那一行删除,或注释掉,就将恢复成原有的功能。是不是很方便呢?
该实现最大的优点就在于不影响原先的使用习惯,而且无论是添加或者删除调试信息都是一件非常简单的事情。这种实现方式其实和java.io包中的一些实现是一样的,有兴趣的朋友可以进行一下对比。
总结
这其实只是一个简单的辅助类,它的应用远没有您在看到本文标题时所想的那么多,其功能也只是简单的扩充,但在特定的时候,却非常的有用。要知道,我写这个类,就是因为在执行一条有差不多40个Double型参数的SQL语句时,报错说数值超出范围了,抓头之余写了这个类,帮了我很大的忙。
同时这也是一个优美的模式应用的范例。在和java.io包进行对比的时候,将会发现两者的结构其实是等同的。模式的学习,最重要是要多多的进行比较,思考程序“为什么”以及“如何”写,这样才会有进步。
参考资料
Sun的JDBC首页:http://java.sun.com/products/jdbc/index.html ,这里有JDBC相关的介绍,教程,规范等。
Sun的JDBC驱动搜索:http://industry.java.sun.com/products/jdbc/drivers ,在这里可以搜索到各种数据库的JDBC驱动,你可能更需要符合JDBC2标准的驱动。
机械工业出版社出版的《设计模式-可复用面向对象软件的基础》(ISBN:7-111-07575-7):这是一本全面讲述设计模式的好书,作者都是业界公认的设计大师,面向对象编程,此书不可不看。
com.beaconsystem.util.db.DebugPreparedStatement的源码可以在这里下载。
关于作者
SonyMusic,计算机世界开发者俱乐部Java编程版版主,目前任职于南京丰柏信息技术有限公司,主要开发语言是Java,致力于仓储、物流软件以及条码应用的开发。您可以通过liu@firstbyte..com.cn和他联系。