摘要
大部分的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、执行语句
6、遍历结果集并且形成结果对象
还有,你必须考虑那些不断产生的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 call Object[] results
= handleQuery(sql, pStmntValues, processor, conn);
//Close the
connection closeConn(conn);
//And return its results return
results; }
protected Object[] handleQuery(String sql, Object[]
pStmntValues, ResultProcessor processor, Connection conn)
{
//Get a prepared statement to use PreparedStatement stmnt =
null;
try {
//Get an actual prepared statement stmnt =
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 statement ResultSet
rs = stmnt.executeQuery();
//Get the results from this
query Object[] results = processor.process(rs);
//Close out the
statement only. The connection will be closed by
the //caller. closeStmnt(stmnt);
//Return the
results return 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
exception closeConn(conn); closeStmnt(stmnt);
//And rethrow
as our runtime exception throw 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
index for(int i = 0; i < values.length; i++) {
//If the
object is our representation of a null value, then handle it
separately if(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 table Column 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
users List users = new ArrayList(); User user =
null;
//If there were results returned, then process
them while(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 query SQLProcessor 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 object return (User)
users[0]; } 这就是全部。我们只需要一个处理类和一个简单的方法,我们就可以无需进行直接的连接维护、语句和异常处理。此外,如果我们拥有另外一个查询由用户表中得到一行,例如通过用户名或者密码,我们可以重新使用UserResultProcessor。我们只需要插入一个不同的SQL语句,并且可以重新使用以前方法的用户处理器。由于返回行的元数据并不依赖查询,所以我们可以重新使用结果处理器。
更新的架构
那么数据库更新又如何呢?我们可以用类似的方法处理,只需要进行一些修改就可以了。首先,我们必须增加两个新的方法到SQLProcessor类。它们类似executeQuery()和handleQuery()方法,除了你无需处理结果集,你只需要将更新的行数作为调用的结果:
public void executeUpdate(String sql, Object[]
pStmntValues, UpdateProcessor processor) {
//Get a
connection Connection conn =
ConnectionManager.getConnection();
//Send it off to be
executed handleUpdate(sql, pStmntValues, processor,
conn);
//Close the
connection closeConn(conn); }
protected void
handleUpdate(String sql, Object[] pStmntValues, UpdateProcessor
processor, Connection conn) {
//Get a prepared statement to
use PreparedStatement stmnt = null;
try {
//Get an actual
prepared statement stmnt = 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 statement int rows
= stmnt.executeUpdate();
//Now hand off the number of rows updated
to the processor processor.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
exception closeConn(conn); closeStmnt(stmnt);
//And rethrow
as our exception throw new
DatabaseUpdateException(message); } }
这些方法和查询处理方法的区别仅在于它们是如何处理调用的结果:由于一个更新的操作只返回更新的行数,因此我们无需结果处理器。我们也可以忽略更新的行数,不过有时我们可能需要确认一个更新的产生。UpdateProcessor获得更新行的数据,并且可以对行的数目进行任何类型的确认或者记录:
public interface UpdateProcessor { public void process(int
rows); } 如果一个更新的调用必须至少更新一行,这样实现UpdateProcessor的对象可以检查更新的行数,并且可以在没有行被更新的时候抛出一个特定的异常。或者,我们可能需要记录下更新的行数,初始化一个结果处理或者触发一个更新的事件。你可以将这些需求的代码放在你定义的UpdateProcessor中。你应该知道:各种可能的处理都是存在的,并没有任何的限制,可以很容易得集成到架构中。 157
|