摘要
大部分的J2EE(Java 2 Platform, Enterprise Edition)和其它类型的Java应用都需要与数据库进行交互。与数据库进行交互需要反复地调用SQL语句、连接管理、事务生命周期、结果处理和异常处理。这些操作都是很常见的;不过这个重复的使用并不是必定需要的。在这篇文章中,我们将介绍一个灵活的架构,它可以解决与一个兼容JDBC的数据库的重复交互问题。
最近在为公司开发一个小的J2EE应用时,我对执行和处理SQL调用的过程感到很麻烦。我认为在Java开发者中一定有人已经开发了一个架构来消除这个流程。不过,搜索诸如\"Java SQL framework" 或者 "JDBC [Java Database Connectivity] framework"等都没有得到满意的结果。
问题的提出?
在讲述一个解决方法之前,我们先将问题描述一下。如果你要通过一个JDBC数据源执行SQL指令时,你通常需要做些什么呢?
1、建立一个SQL字符串
2、得到一个连接
3、得到一个预处理语句(prepared statement)
4、将值组合到预处理语句中
5、执行语句\r
6、遍历结果集并且形成结果对象\r
还有,你必须考虑那些不断产生的SQLExceptions;如果这些步骤出现不同的地方,SQLExecptions的开销就会复合在一起,因为你必须使用多个try/catch块。
不过,如果我们仔细地观察一下这些步骤,就可以发现这个过程中有几个部分在执行期间是不变的:你通常都使用同一个方式来得到一个连接和一个预处理语句。组合预处理语句的方式通常也是一样的,而执行和处理查询则是特定的。你可以在六个步骤中提取中其中三个。即使在有点不同的步骤中,我们也可以在其中提取出公共的功能。但是我们应该怎样自动化及简化这个过程呢?
查询架构
我们首先定义一些方法的签名,这些方法是我们将要用来执行一个SQL语句的。要注意让它保持简单,只传送需要的变量,我们可以编写一些类似下面签名的方法:
public Object[] executeQuery(String sql, Object[] pStmntValues,ResultProcessor processor);
我们知道在执行期间有所不同的方面是SQL语句、预处理语句的值和结果集是如何分析的。很明显,sql参数指的是SQL语句。pStmntValues对象数据包含有必须插入到预处理语句中的值,而processor参数则是处理结果集并且返回结果对象的一个对象;我将在后面更详细地讨论这个对象。
在这样一个方法签名中,我们就已经将每个JDBC数据库交互中三个不变的部分隔离开来。现在让我们讨论exeuteQuery()及其它支持的方法,它们都是SQLProcessor类的一部分:
public class SQLProcessor {public Object[] executeQuery(String sql, Object[] pStmntValues,ResultProcessor processor) {//Get a connection (assume it's part of a ConnectionManager class)Connection conn = ConnectionManager.getConnection();//Hand off our connection to the method that will actually execute//the callObject[] results = handleQuery(sql, pStmntValues, processor, conn);//Close the connectioncloseConn(conn);//And return its resultsreturn results;}protected Object[] handleQuery(String sql, Object[] pStmntValues,ResultProcessor processor, Connection conn) {//Get a prepared statement to usePreparedStatement stmnt = null;try {//Get an actual prepared statementstmnt = conn.prepareStatement(sql);//Attempt to stuff this statement with the given values. If//no values were given, then we can skip this step.if(pStmntValues != null) {PreparedStatementFactory.buildStatement(stmnt, pStmntValues);}//Attempt to execute the statementResultSet rs = stmnt.executeQuery();//Get the results from this queryObject[] results = processor.process(rs);//Close out the statement only. The connection will be closed by the//caller.closeStmnt(stmnt);//Return the resultsreturn results;//Any SQL exceptions that occur should be recast to our runtime query//exception and thrown from here} catch(SQLException e) {String message = "Could not perform the query for " + sql;//Close out all resources on an exceptioncloseConn(conn);closeStmnt(stmnt);//And rethrow as our runtime exceptionthrow new DatabaseQueryException(message);}}}...}
在这些方法中,有两个部分是不清楚的:PreparedStatementFactory.buildStatement() 和 handleQuery()'s processor.process()方法调用。buildStatement()只是将参数对象数组中的每个对象放入到预处理语句中的相应位置。例如:
...//Loop through all objects of the values array, and set the value//of the prepared statement using the value array indexfor(int i = 0; i < values.length; i++) {//If the object is our representation of a null value, then handle it separatelyif(value instanceof NullSQLType) {stmnt.setNull(i + 1, ((NullSQLType) value).getFieldType());} else {stmnt.setObject(i + 1, value);}}
由于stmnt.setObject(int index, Object value)方法不可以接受一个null对象值,因此我们必须使用自己特殊的构造:NullSQLType类。NullSQLType表示一个null语句的占位符,并且包含有该字段的JDBC类型。当一个NullSQLType对象实例化时,它获得它将要代替的字段的SQL类型。如上所示,当预处理语句通过一个NullSQLType组合时,你可以使用NullSQLType的字段类型来告诉预处理语句该字段的JDBC类型。这就是说,你使用NullSQLType来表明正在使用一个null值来组合一个预处理语句,并且通过它存放该字段的JDBC类型。
现在我已经解释了PreparedStatementFactory.buildStatement()的逻辑,我将解释另一个缺少的部分:processor.process()。processor是ResultProcessor类型,这是一个接口,它表示由查询结果集建立域对象的类。ResultProcessor包含有一个简单的方法,它返回结果对象的一个数组:
public interface ResultProcessor {public Object[] process(ResultSet rs) throws SQLException;}
一个典型的结果处理器遍历给出的结果集,并且由结果集合的行中形成域对象/对象结构。现在我将通过一个现实世界中的例子来综合讲述一下。
查询例子
你经常都需要利用一个用户的信息表由数据库中得到一个用户的对象,假设我们使用以下的USERS表:
USERS tableColumn Name Data Type ID NUMBER USERNAME VARCHAR F_NAME VARCHAR L_NAME VARCHAR EMAIL VARCHAR
并且假设我们拥有一个User对象,它的构造器是:
public User(int id, String userName, String firstName,
String lastName, String email)
如果我们没有使用这篇文章讲述的架构,我们将需要一个颇大的方法来处理由数据库中接收用户信息并且形成User对象。那么我们应该怎样利用我们的架构呢?
首先,我们构造SQL语句:
private static final String SQL_GET_USER = "SELECT * FROM USERS WHERE ID = ?";
接着,我们形成ResultProcessor,我们将使用它来接受结果集并且形成一个User对象:
public class UserResultProcessor implements ResultProcessor {//Column definitions here (i.e., COLUMN_USERNAME, etc...)..public Object[] process(ResultSet rs) throws SQLException {//Where we will collect all returned usersList users = new ArrayList();User user = null;//If there were results returned, then process themwhile(rs.next()) {user = new User(rs.getInt(COLUMN_ID), rs.getString(COLUMN_USERNAME),rs.getString(COLUMN_FIRST_NAME), rs.getString(COLUMN_LAST_NAME),rs.getString(COLUMN_EMAIL));users.add(user);}return users.toArray(new User[users.size()]);
最后,我们将写一个方法来执行查询并且返回User对象:
public User getUser(int userId) {//Get a SQL processor and execute the querySQLProcessor processor = new SQLProcessor();Object[] users = processor.executeQuery(SQL_GET_USER_BY_ID,new Object[] {new Integer(userId)},new UserResultProcessor());//And just return the first User objectreturn (User) users[0];}
这就是全部。我们只需要一个处理类和一个简单的方法,我们就可以无需进行直接的连接维护、语句和异常处理。此外,如果我们拥有另外一个查询由用户表中得到一行,例如通过用户名或者密码,我们可以重新使用UserResultProcessor。我们只需要插入一个不同的SQL语句,并且可以重新使用以前方法的用户处理器。由于返回行的元数据并不依赖查询,所以我们可以重新使用结果处理器。
更新的架构
那么数据库更新又如何呢?我们可以用类似的方法处理,只需要进行一些修改就可以了。首先,我们必须增加两个新的方法到SQLProcessor类。它们类似executeQuery()和handleQuery()方法,除了你无需处理结果集,你只需要将更新的行数作为调用的结果:
public void executeUpdate(String sql, Object[] pStmntValues,UpdateProcessor processor) {//Get a connectionConnection conn = ConnectionManager.getConnection();//Send it off to be executedhandleUpdate(sql, pStmntValues, processor, conn);//Close the connectioncloseConn(conn);}protected void handleUpdate(String sql, Object[] pStmntValues,UpdateProcessor processor, Connection conn) {//Get a prepared statement to usePreparedStatement stmnt = null;try {//Get an actual prepared statementstmnt = conn.prepareStatement(sql);//Attempt to stuff this statement with the given values. If//no values were given, then we can skip this step.if(pStmntValues != null) {PreparedStatementFactory.buildStatement(stmnt, pStmntValues);}//Attempt to execute the statementint rows = stmnt.executeUpdate();//Now hand off the number of rows updated to the processorprocessor.process(rows);//Close out the statement only. The connection will be closed by the//caller.closeStmnt(stmnt);//Any SQL exceptions that occur should be recast to our runtime query//exception and thrown from here} catch(SQLException e) {String message = "Could not perform the update for " + sql;//Close out all resources on an exceptioncloseConn(conn);closeStmnt(stmnt);//And rethrow as our exceptionthrow new DatabaseUpdateException(message);}}
这些方法和查询处理方法的区别仅在于它们是如何处理调用的结果:由于一个更新的操作只返回更新的行数,因此我们无需结果处理器。我们也可以忽略更新的行数,不过有时我们可能需要确认一个更新的产生。UpdateProcessor获得更新行的数据,并且可以对行的数目进行任何类型的确认或者记录:
public interface UpdateProcessor {public void process(int rows);}
如果一个更新的调用必须至少更新一行,这样实现UpdateProcessor的对象可以检查更新的行数,并且可以在没有行被更新的时候抛出一个特定的异常。或者,我们可能需要记录下更新的行数,初始化一个结果处理或者触发一个更新的事件。你可以将这些需求的代码放在你定义的UpdateProcessor中。你应该知道:各种可能的处理都是存在的,并没有任何的限制,可以很容易得集成到架构中。
更新的例子
我将继续使用上面解释的User模型来讲述如何更新一个用户的信息:
首先,构造SQL语句:
private static final String SQL_UPDATE_USER = "UPDATE USERS SET USERNAME = ?, " +"F_NAME = ?, " +"L_NAME = ?, " +"EMAIL = ? " +"WHERE ID = ?";
接着,构造UpdateProcessor,我们将用它来检验更新的行数,并且在没有行被更新的时候抛出一个异常:
public class MandatoryUpdateProcessor implements UpdateProcessor {public void process(int rows) {if(rows < 1) {String message = "There were no rows updated as a result of this operation.";throw new IllegalStateException(message);}}}
最后就写编写执行更新的方法:
public static void updateUser(User user) {SQLProcessor sqlProcessor = new SQLProcessor();//Use our get user SQL statementsqlProcessor.executeUpdate(SQL_UPDATE_USER,new Object[] {user.getUserName(),user.getFirstName(),user.getLastName(),user.getEmail(),new Integer(user.getId())},new MandatoryUpdateProcessor());
如前面的例子一样,我们无需直接处理SQLExceptions和Connections就执行了一个更新的操作。
事务\r
前面已经说过,我对其它的SQL架构实现都不满意,因为它们并不拥有预定义语句、独立的结果集处理或者可处理事务。我们已经通过buildStatement() 的方法解决了预处理语句的问题,还有不同的处理器(processors)已经将结果集的处理分离出来。不过还有一个问题,我们的架构如何处理事务呢?
一个事务和一个独立SQL调用的区别只是在于在它的生命周期内,它都使用同一个连接,还有,自动提交标志也必须设置为off。因为我们必须有一个方法来指定一个事务已经开始,并且在何时结束。在整个事务的周期内,它都使用同一个连接,并且在事务结束的时候进行提交。
要处理事务,我们可以重用SQLProcessor的很多方面。为什么将该类的executeUpdate() 和handleUpdate()独立开来呢,将它们结合为一个方法也很简单的。我这样做是为了将真正的SQL执行和连接管理独立开来。在建立事务系统时,我们必须在几个SQL执行期间对连接进行控制,这样做就方便多了。
为了令事务工作,我们必须保持状态,特别是连接的状态。直到现在,SQLProcessor还是一个无状态的类。它缺乏成员变量。为了重用SQLProcessor,我们创建了一个事务封装类,它接收一个SQLProcessor并且透明地处理事务的生命周期。
具体的代码是:
public class SQLTransaction {private SQLProcessor sqlProcessor;private Connection conn;//Assume constructor that initializes the connection and sets auto commit to false...public void executeUpdate(String sql, Object[] pStmntValues,UpdateProcessor processor) {//Try and get the results. If an update fails, then rollback//the transaction and rethrow the exception.try {sqlProcessor.handleUpdate(sql, pStmntValues, processor, conn);} catch(DatabaseUpdateException e) {rollbackTransaction();throw e;} }public void commitTransaction() {//Try to commit and release all resourcestry {conn.commit();sqlProcessor.closeConn(conn);//If something happens, then attempt a rollback and release resources} catch(Exception e) {rollbackTransaction();throw new DatabaseUpdateException("Could not commit the current transaction.");}}private void rollbackTransaction() {//Try to rollback and release all resourcestry {conn.rollback();conn.setAutoCommit(true);sqlProcessor.closeConn(conn);//If something happens, then just swallow it} catch(SQLException e) {sqlProcessor.closeConn(conn);}}}
SQLTransaction拥有许多新的方法,但是其中的大部分都是很简单的,并且只处理连接或者事务处理。在整个事务周期内,这个事务封装类只是在SQLProcessor中增加了一个简单的连接管理。当一个事务开始时,它接收一个新的连接,并且将其自动提交属性设置为false。其后的每个执行都是使用同一个连接(传送到SQLProcessor的handleUpdate()方法中),因此事务保持完整。
只有当我们的持久性对象或者方法调用commitTransaction()时,事务才被提交,并且关闭连接。如果在执行期间发生了异常,SQLTransaction可以捕捉该异常,自动进行回滚,并且抛出异常。
事务例子
让我们来看一个简单的事务\r
//Reuse the SQL_UPDATE_USER statement defined abovepublic static void updateUsers(User[] users) {//Get our transactionSQLTransaction trans = sqlProcessor.startTransaction();//For each user, update itUser user = null;for(int i = 0; i < users.length; i++) {user = users[i];trans.executeUpdate(SQL_UPDATE_USER,new Object[] {user.getUserName(),user.getFirstName(),user.getLastName(),user.getEmail(),new Integer(user.getId())},new MandatoryUpdateProcessor());}//Now commit the transactiontrans.commitTransaction();}
上面为我们展示了一个事务处理的例子,虽然简单,但我们可以看出它是如何工作的。如果在执行executeUpdate()方法调用时失败,这时将会回滚事务,并且抛出一个异常。调用这个方法的开发者从不需要担心事务的回滚或者连接是否已经关闭。这些都是在后台处理的。开发者只需要关心商业的逻辑。
事务也可以很轻松地处理一个查询,不过这里我没有提及,因为事务通常都是由一系列的更新组成的。
问题\r
在我写这篇文章的时候,对于这个架构,我提出了一些疑问。这里我将这些问题提出来,因为你们可能也会碰到同样的问题。
自定义连接
如果每个事务使用的连接不一样时会如何?如果ConnectionManager需要一些变量来告诉它从哪个连接池得到连接?你可以很容易就将这些特性集合到这个架构中。executeQuery() 和 executeUpdate()方法(属于SQLProcessor和SQLTransaction类)将需要接收这些自定义的连接参数,并且将他们传送到ConnectionManager。要记得所有的连接管理都将在执行的方法中发生。
此外,如果更面向对象化一点,连接制造者可以在初始化时传送到SQLProcessor中。然后,对于每个不同的连接制造者类型,你将需要一个SQLProcessor实例。根据你连接的可变性,这或许不是理想的做法。
ResultProcessor返回类型
为什么ResultProcessor接口指定了process()方法应该返回一个对象的数组?为什么不使用一个List?在我使用这个架构来开发的大部分应用中,SQL查询只返回一个对象。如果构造一个List,然后将一个对象加入其中,这样的开销较大,而返回一个对象的一个数组是比较简单的。不过,如果在你的应用中需要使用对象collections,那么返回一个List更好。
SQLProcessor初始管理\r
在这篇文章的例子中,对于必须执行一个SQL调用的每个方法,初始化一个SQLProcessor。由于SQLProcessors完全是没有状态的,所以在调用的方法中将processor独立出来是很有意义的。
而对于SQLTransaction类,则是缺少状态的,因此它不能独立使用。我建议你为SQLProcessor类增加一个简单的方法,而不是学习如何初始化一个SQLTransaction,如下所示:
public SQLTransaction startTransaction() {
return new SQLTransaction(this);
}
这样就会令全部的事务功能都在SQLProcessor类中访问到,并且限制了你必须知道的方法调用。
数据库异常
我使用了几种不同类型的数据库异常将全部可能在运行时发生的SQLExceptions封装起来。在我使用该架构的应用中,我发现将这些异常变成runtime exceptions更为方便,所以我使用了一个异常处理器。你可能认为这些异常应该声明,这样它们可以尽量在错误的发生点被处理。不过,这样就会令SQL异常处理的流程和以前的SQLExceptions一样,这种情况我们是尽量避免的。
省心的JDBC programming
这篇文章提出的架构可以令查询、更新和事务执行的操作更加简单。在类似的SQL调用中,你只需要关注可重用的支持类中的一个方法。我的希望是该架构可以提高你进行JDBC编程的效率。