<< Java Code Static Analysis(Java代码静态分析工具) | 首页 | 部署yale CAS 服务器定制JDBC 数据库验证 >>

使用 Spring 框架来改善 Oracle JDBC 访问

JDBC 是一个常用于访问关系数据库的标准数据访问协议。JDBC 的一个显著优势是其标准化的 API,为基于 Java 的数据访问应用程序的可移植性奠定了基础。JDBC 是标准 Java (J2SE) 和企业 Java (J2EE) 中一个不可或缺的部分,在 Java 早期阶段就已推出。

JDBC 有许多优势,使得它能够在许多 J2SE 和 J2EE 应用程序中发挥重要作用。但它也有一些不足之处,使得我们不能称心如意的使用它们。这些麻烦(有时候让人厌恶)的 JDBC 特性催生出了许多公开的 JDBC 抽象框架(例如 SQLExecutor 和 Apache Jakarta Commons DBUtil)以及更多得多的自主开发的 JDBC 应用程序框架。Spring 框架的 JDBC 抽象就是一个公开的 JDBC 抽象框架。

Spring 框架是一个在 Apache 许可下发布的 Java/J2EE 应用程序框架,它支持 J2EE 应用程序中的多个层次。Spring 框架的一个突出特性是支持更易于维护和更强健的 JDBC 数据访问。在本文中,您将了解到 Spring 框架 — 它可以和 Oracle TopLink 对象/关系映射工具结合使用 — 如何大大减少与编写 JDBC 代码相关的烦琐工作和风险。使用 Spring 框架,开发人员编写的 Oracle 数据库访问 JDBC 代码可以更为简洁、更不易出错以及更加灵活。

正确关闭数据库资源

JDBC 代码中的一个常见错误是没有正确关闭连接。这将导致数据库资源的不合理分配。类似地,关闭结果集和语句也是有用并通常推荐的操作。为了确保即使在异常的运行条件下也能正确执行这些关闭操作,一般将采用代码清单 1 中 finally 子句中的代码。

代码清单 1

try
{
// JDBC Connection/Statement/Result Set
}
catch (SQLException sqlEx)
{
// Handle the exception
}
finally
{
try
{ // Closing connection *should* close statement and result set
if (stmt != null) stmt.close();
if (conn != null) conn.close();
  }
catch (SQLException sqlEx)
  {
System.err.println("SQLException NOT handled");
  }
}
finally   子句通常被用来确保关闭数据库连接和语句。但即使当开发人员的确用这种方法确保成功关闭连接,
代码也是冗长、膨胀和重复的。Spring   框架对连接处理和相关资源管理进行了抽象,开发人员不用直接处理上述事项,
从而实现更一致的资源关闭并编写更易于理解的代码。  
 
第一个 Spring 代码示例   
代码清单 2 中的 JDBC 代码可以用
来查询(大家都熟悉)的 scott/tiger   模式中的员工的酬金。正如之前所讨论的那样,在本示例中除了实际查询数据库的
 SQL 代码之外,还必需要有大量的“例行”代码。

代码清单 2

List commissions = new ArrayList();
Statement stmt = null;
ResultSet rs = null;
try
{
stmt = this.myConnection.createStatement();
rs = stmt.executeQuery("SELECT comm FROM emp"); 
while ( rs.next() )
  {
Integer commission = new Integer( rs.getInt("COMM") );
if ( rs.wasNull() )
   {
// By assigning the commission to null, this effectively
// represents a null in the database as a Java null.
System.out.println( "\tCommission seen as " + commission +
" is really null");
commission = null;
   }
commissions.add( commission );
  }
}
catch (SQLException sqlEx)  // checked
{
System.err.println( "Message:" + sqlEx.getMessage() );
System.err.println( "Error Code:" + sqlEx.getErrorCode() );
System.err.println( "SQL State:" + sqlEx.getSQLState() );
}
finally
{
try
  {
if ( rs != null ) { rs.close(); }
if ( stmt != null ) { stmt.close(); }
  }
catch (SQLException sqlEx) // checked
  {
System.err.println( sqlEx.getMessage() );
  }
}
代码清单 3 中为使用 Spring 框架的代码,它提供了类似于代码清单 2 的功能。

代码清单 3

List commissions = new ArrayList();
try
{
JdbcTemplate jt = new JdbcTemplate(this.myDataSource);
List commList = jt.queryForList( "SELECT comm FROM emp");
  
Iterator commIter = commList.iterator();
while ( commIter.hasNext() )
  {
Number comm = (Number) ((Map) commIter.next()).get("COMM");
if (comm != null)
commissions.add( new Integer(comm.intValue()) );
else commissions.add( null );  } 
}
catch ( DataAccessException ex )  // unchecked exception
{
System.err.println( ex.getMessage() );
}
     

值得注意的是与直接使用 JDBC 相比,利用 Spring 框架可以少得多的代码实现同样的功能。如代码清单 3 所示,您不需要编写和维护管理资源(连接、语句、结果集)的代码。甚至代码清单 3 中的少量的异常处理代码也不是绝对必需的,因为 DataAccessException 是一个非强制异常。因为 Number 类型用来返回奖金,因此不需要显式调用 ResultSet 的 wasNull 方法。实际上,您甚至在代码清单 3 中的任何地方都找不到 ResultSet 语法!
代码清单 3 还说明了由 Spring 框架的 JDBC 支持所提供和使用的基础类之一 — JdbcTemplate。我们将使用一个数据源来完成这个由 Spring 提供的类的实例化,然后在模板类上使用提供的 SQL 字符串调用它的被覆盖的 queryForList 方法之一。queryForList 方法将返回一个包含 HashMap 的 ArrayList,其中该 ArrayList 中的每一个元素都是一个返回的数据行,一个特定数组阵列元素中的每一个 Map 条目都是该行中的一个列值。
JdbcTemplate 提供了许多被覆盖的 queryForList 方法,它们可以用来查询潜在的多行数据。这个非常有用的类还提供了诸如 queryForInt(返回单个整数)、queryForLong(返回单个 long 型整数)、query、update 之类的方法。要分辨这些不同的被覆盖的方法,最容易的方式是阅读与 Spring 框架一起提供的基于 Javadoc 的 API 文档中的“方法详情”部分。这些方法的不同点在于使用的语句的类型(例如 Statement 或 PreparedStatement)和支持的特性。JdbcTemplate 还提供了一些方法,与上面使用的方法相比,它们需要更多的 JDBC 知识,但它们提供了更好的灵活性。这些更灵活但需要更多 JDBC 知识的方法将在本文稍后进行说明。


JDBC 异常处理


返回到代码清单 1,注意 java.sql.SQLException 是唯一一个显式捕获的异常。SQLException 中捕获了与数据库和 SQL 相关的各种异常情况。描述 SQLException 类的 Javadoc 注释介绍了可以从 SQLException 的实例中获得的基本信息。这些信息包括错误描述字符串 [getMessage()]、某个标准化 SQLState 异常 String [getSQLState()] 和供应商特有的整型错误码 [getErrorCode()]。在代码清单 1 中实现的简单的异常处理中使用了所有这三种信息。
SQLException 是一种强制异常(直接扩展 java.lang.Exception)。Java 的强制异常曾经引起很大争议,现在 Java 社区似乎正在取得共识:只有当在应用程序能够处理异常时才应使用强制异常。如应用程序代码不能以有意义的方式处理异常,则不应当强制处理该异常。因为 SQLException 是强制异常,所以应用程序代码必须处理它,或者捕获它并对其进行一些处理或显式地将其抛出给调用代码。
SQLException 的最后一点细微差别在于它是使用 SQL 的关系数据源所特有的异常。这使得不适合将它包含在真正可移植的数据访问对象 (DAO) 中,后者应当独立于数据信息库类型和访问语言。
Spring 框架对 SQLException 的处理是其在支持更容易的 JDBC 开发和维护方面最有用的特性之一。Spring 框架提供了完成 SQLException 抽象化的 JDBC 支持,并提供了一个对 DAO 友好的非检查异常层次结构。
图 1 (利用 Oracle JDeveloper 10g 的 UML 建模工具绘制)图示了一些用于 JDBC 和 DAO 的最有趣和最重要的 Spring 框架异常类。下面的图 1 中显示的所有类都属于 org.springframework 程序包下的一个子程序包。这些 JDBC 特有的异常处理类都位于 jdbc 子程序包中,更通用的 DAO 异常处理类包含在 dao 子程序包中。

图 1

处理供应商特有的错误码

如上所述,标准的 SQLException 提供了一个标准化的信息段 (SQLState) 和一个供应商特有的信息段 (ErrorCode)。正如大多数的数据库和它们的 JDBC 驱动程序实现一样,

Oracle 数据库和 JDBC 驱动程序通过供应商特有的错误码所提供的关于问题的详细信息要比通过 SQLException 的与供应商无关的 SQLState 组件所提供的信息多得多。 Oracle 数据库及其 JDBC 驱动程序通过 Error Code 提供的更丰富得多的详细信息的一个明显的例子是 SQLState 代码 42000 (通常这指示语法错误或访问问题)。对于 Oracle JDBC 驱动程序的大量不同的 Oracle 错误码,SQLException 都将返回相同的 SQLState 值 (42000)。对应 SQLState 的 42000 值的一些 Oracle 错误码包括 900(“无效 SQL 语句”)、903(“无效表名”)、904(“无效标识符”)、911(“无效字符”)和 936(“缺少表达式”)。此外很明显任何来源于 Oracle JDBC 驱动程序的错误(与来源于 Oracle 数据库的错误相反)都没有任何对应的 SQLState。正如这些例子所显示那样,Oracle 特有的错误码所提供的关于错误情况的详细信息要比与供应商无关的 SQLState 所提供的信息更丰富得多。 有时候区分来源于 Oracle 数据库的错误和来源于 Oracle JDBC 驱动程序的错误非常重要。例如,903(“无效的表名”)错误码对应 Oracle 数据库错误码 ORA-00903。相反,17003(“无效的列索引”)错误码对应 Oracle JDBC 驱动程序错误码 ORA-17003。这两种类型的错误码(数据库和 JDBC 驱动程序)都是 Oracle 特有的(如 ORA- 前缀所指示的那样)。因为没有为来源于 Oracle JDBC 驱动程序的错误提供 SQLState 错误码,因此必须使用 Oracle 特有的错误码来区分由 JDBC 驱动程序导致的错误。

在下面的表 1 中列出了访问 Oracle 数据库的 JDBC 中的一些最常见的错误。“示例代码和/或注释”列中的示例显示了导致这种错误的 SQL 语句类型或提供了关于表中的该行上显示的特定错误的其他注释。

表 1
错误标记 Oracle 错误 SQLState 示例代码和/或注释

基于语句:SELECT ename FROM emp 变种的 SQL 相关错误

“唯一性约束” 1 2300 例如主键违规

“资源忙且指定 NOWAIT 获取资源”

54 61000 只有在指定了 NOWAIT 时才出现

“无效的 SQL 语句”

900 42000

ename FROM emp

“无效的表名”

903 42000

SELECT ename FROM

“无效的标识符”

904 42000

SELECT empname FROM emp

“无效的字符”

911 42000

SELECT ename FROM emp;

“缺少列”

917 42000

在 INSERT 语句中需要逗号来分隔列时遗漏逗号可能是一个原因。

“在期望的位置没有找到 FROM 关键字”

923 42000

SELECT ename emp

“缺少表达式”

936 42000

SELECT FROM emp

“表或视图不存在”

942 42000

SELECT ename FROM empp

“不能插入空值”

1400 23000 试图向包含 NOT NULL 约束的列中插入空值

“值大于该列的指定精度”

1438 22003 试图插入比列允许的精度更多的数字位数

“无效的数字”

1722 42000 试图对字符执行数值函数

“完整性约束失败”

2291 23000 试图插入包含与现有主键不匹配的外键的行

“值太大,”

12899 72000 试图插入超出列允许范围的的值(例如过多的字符)

“Io 异常”

17002 来源于 Oracle JDBC 驱动程序的错误没有对应的 SQLState (null)

“无效的列索引”

17003

 

“无效的列名”

17006

 

“数值溢出”

17026

 

本文末尾的在线资源部分包含了到一个网站的链接,该网站详细介绍了用户可能遇到的各种 Oracle 数据库异常。Oracle JDBC 驱动程序错误码可以在 Oracle JDBC 开发人员指南和参考的附录 B 中找到,几种常见的 Oracle 数据库产生的错误码可以在 Oracle 数据库错误消息文档中找到(没有列出产品特有的 ORA 消息)。

Spring 框架既支持基于标准的 SQLState 又支持供应商特有的错误码。与自主开发的数据访问软件相比,该框架对供应商特有的

错误码的支持利用了与特有数据库的更松散的耦合来实现。Spring 框架引入了一个 XML 配置文件,利用它将在 JDBC 代码中经常遇到的某些供应商特有的错误与 Spring 支持的异常类连接起来。Spring 提供的 sql-error-codes.xml 配置文件目前包含了代码清单 4 中所示针对 Oracle 数据库的配置。(在该文件中还涉及其他的数据库供应商,但并包含在此代码清单中。)

代码清单 4

<bean id="Oraclea€?
class="org.springframework.jdbc.support.SQLErrorCodes">
<property name="badSqlGrammarCodes">
<value>900,903,904,917,936,942,17006</value>
</property>
<property name="invalidResultSetAccessCodes">
<value>17003</value>
</property>
<property name="dataAccessResourceFailureCodes">
<value>17002</value>
</property>
<property name="dataIntegrityViolationCodes">
<value>1,1400,1722,2291</value>
</property>
<property name="cannotAcquireLockCodes">
<value>54</value>
</property>
</bean>

sql-error-codes.xml 中的 value 元素的主体中的整数由逗号分隔,它们对应于前面所述的供应商特有的错误码的数字部分。

表 1 中列出了 "badSqlGrammarCodes" 类别中的许多数字码。17006 代码是一个 JDBC 驱动程序错误码,它指示“无效的列名”。代码清单 4 中的 property 元素标记的 name 属性指示 Spring 框架使用哪种类型的异常来处理那些特定的错误码。例如,917 (ORA-00917) 错误将导致 Spring 框架抛出一个非强制的 BadSqlGrammarException。因为该配置文件是 XML 格式的并且在代码的外部,因此可以很容易地将其他代码添加到该文件中,用于抛出最适合于特定供应商错误码的基于 Spring 的 JDBC 异常。 出于各种原因,您可能希望抛出与数据库的错误码对应的特定异常。例如,您可能希望选择处理 SQLException 正常抛出的情况,而不是处理所有的情况。因为在许多情况下,在运行时您无法对代码作任何处理。通过为数据库开发人员创建一个更细粒化的异常层次结构,以及通过提供特定数据库错误和特定异常之间的一个松散耦合的连接,Spring 框架使您能够更轻松地处理那些容易处理的异常,而选择忽略不能合理处理的非强制异常。 Spring 为 JDBC 支持提供的特别方便的异常类之一是 BadSqlGrammarException。该异常类提供了一个名称为 getSql() 的方法,该方法将返回在抛出异常时正被调用的 SQL 语句。因为该类可以识别 SQL 的特征(它不是一个通用的 DAO 类),因此它还通过 getSQLException() 方法提供了标准 SQLException 的一个句柄。 除了将其他的 Oracle 特有的错误码添加到 sql-error-codes.xml 文件中以将它们映射至现有的由 Spring 提供的异常类之外,您还可以创建定制的异常处理类。然后可以编写一个定制的 SQLExceptionTranslator 类来将 Oracle 错误码与这些定制的异常处理类连接起来。

将 PreparedStatement 用于 Spring

代码清单 3 中的 Spring 示例依靠 Spring 的 Statement 包装来执行 SQL 语句。然而,通常推荐使用 PreparedStatement 而不是 Statement 来对数据库执行 SQL 语句。Spring JdbcTemplate 类为许多方法提供了在 Statement 和 PreparedStatement 两者上构建的相同的功能,这样便于您按需选择 JDBC 语句的底层类型。 Spring 的基于 Javadoc 的 API 文档详细说明了各个方法是使用 Statement 还是 PreparedStatement。您还可以根据是否和 SQL 字符串一起传递了 SQL 参数给方法来分辨 JdbcTemplate 使用了哪一种类型。如果只传入了 SQL 字符串,那么方法一般使用 Statement。如果方法接收了 SQL 字符串的参数以及 SQL 语句,那么方法一般使用 PreparedStatement。下面的两个代码清单(代码清单 5 和代码清单 6)显示了使用 PreparedStatement 的标准 JDBC 访问和包装 PreparedStatement 的基于 Spring 的访问。

代码清单 5

String updateStr =
"UPDATE salgrade SET losal = ?, hisal = ?WHERE grade = ?";
PreparedStatement stmt = null;
   
try
{
stmt = this.myConnection.prepareStatement(updateStr);
stmt.setInt(1,aLowSal);
stmt.setInt(2,aHighSal);
stmt.setInt(3,aSalGrade);
     
updateStatus = stmt.execute();
stmt.close();
} // lots of catch and finally code typically follows here

 

 

代码清单 6

String updateStr =
"UPDATE salgrade SET losal = ?, hisal = ?WHERE grade = ?";
 
JdbcTemplate jt = new JdbcTemplate(this.myDataSource);
jt.update( updateStr,
new Object[] { new Integer(aLowSal),
new Integer(aHighSal),
new Integer(aSalGrade) } );
// No handling/closing of PreparedStatement or catch/finally needed

代码清单 6 为使用 Spring 框架来更新数据库的一个示例。虽然 "PreparedStatement" 语义没有在代码清单中出现,

但本例中使用的 JdbcTemplate 的特殊的更新方法的确使用了 PreparedStatement。JDBC 的标准用法要求捕获 SQLException,并且必需一个 finally 程序块来确保关闭语句。代码清单 6 中的基于 Spring 的代码则不需要这些。 注意在代码清单 6 中的可执行代码中没有显式地提及 PreparedStatement。使用这个方便的 JdbcTemplate 更新方法的开发人员不需要关心 PreparedStatement 的具体用法、其 API 或 SQLException。注意开发人员使用了一个匿名内部类,以提供要和 SQL 字符串一起传递给 JdbcTemplate.update 方法的值。

使用 Oracle 特有的 SQL 语句

Spring 框架的一个有用的特点是它仅专注于“包装”JDBC 开发的最常用和最麻烦的方面,而不会过度阻止在需要的时候使用专有的 SQL/JDBC。虽然我们都希望使我们的代码完全标准化(如果这样做对我们无任何影响或有些一定影响),但有很多时候使用特殊的供应商特有的特性将是谨慎甚至必须的做法。 在 Oracle 范畴中的一个示例就是使用 Oracle 的 ROWID 来唯一描述 Oracle 表中的行。代码清单 7 和 8 显示了传统的基于 JDBC 和 Spring 的 JDBC 代码,它们分别根据提供的员工号从 scott/tiger EMP 表中检索 ROWID。在两种情况下都提供了一个作为字符串返回的 ROWID。

代码清单 7

String queryStr = "SELECT rowid FROM emp WHERE empno = "
+ aEmpNum;  // aEmpNum set previously
String rowId = null;
try
{
stmt = this.myConnection.createStatement();
rs = stmt.executeQuery(queryStr);
while ( rs.next() )
  {
rowId = rs.getString("ROWID");
  }
} // lots of catch-finally code needed after this

 

 

代码清单 8

String queryStr = "SELECT rowid FROM emp WHERE empno = "
+ aEmpNum;
String rowId = null;
  
try
{
JdbcTemplate jt = new JdbcTemplate(this.myDataSource);
oracle.sql.ROWID oraRowId =
(ROWID) jt.queryForObject(queryStr, ROWID.class);
rowId = oraRowId.stringValue();
}
catch ( IncorrectResultSizeDataAccessException wrongSizeEx )
{
// This unchecked exception is thrown in this case if more
// than one result is returned from the query.
// Explicitly printing out the results of this exception's
// methods getExpectedSize() and getActualSize() is really not
// necessary in this case because this exception's getMessage()
// returns this same information in sentence form. 
System.err.println( wrongSizeEx.getMessage() );
System.err.print( "Expected " + wrongSizeEx.getExpectedSize()
+ " results, but actually got back "
+ wrongSizeEx.getActualSize() + " results.");
}

除了显示 Spring 框架支持 Oracle 特有的关键字的灵活性之外,代码清单 8 还显示了 Spring 的 DAO 异常之一的用途。在代码清单 8 中,

如果不小心编辑了 queryStr 来返回所有的 ROWID,那么将抛出 IncorrectResultSizeDataAccessException。 专有 Oracle SQL 的最为大家所熟悉的例子可能是无处不在的查询 SELECT sysdate FROM dual。代码清单 9 显示了这个 Oracle 特有的查询(不是 ANSI 标准的一部分)如何与 Spring 框架一起使用。

代码清单 9

String queryStr = "SELECT sysdate FROM dual";
Date date = null;
  
try
{
JdbcTemplate jt = new JdbcTemplate(this.myDataSource);
date = (Date) jt.queryForObject(queryStr, Date.class);
}
catch ( BadSqlGrammarException badSqlEx ) // unchecked
{
System.err.println( badSqlEx.getMessage() );
System.err.println( "Bad SQL:" + badSqlEx.getSql() );
}

DDL 语句与 Spring 和 JDBC 前面的代码代码清单演示了使用 Spring 框架来处理 DML 语句。Spring 框架提供了非常简单的语义来支持 DDL 语句。

代码清单 10 和 11 演示了用来执行从 Oracle 数据库中删除和清除表的语句的标准 JDBC 代码和 Spring 包装的 JDBC 代码。

代码清单 10

Statement stmt = null;
 
try
{
stmt = this.myConnection.createStatement();
stmt.executeUpdate("DROP TABLE salgrade PURGE");
}
catch ( SQLException sqlEx )
{
System.err.println("Message:" + sqlEx.getMessage());
System.err.println("Error Code:" + sqlEx.getErrorCode());
System.err.println("SQL State:" + sqlEx.getSQLState());
}
finally
{
try
  {
if (stmt != null)
   {
stmt.close();
   }
  }
catch (SQLException finallySqlEx)  // checked exception
  {
System.err.println(finallySqlEx.getMessage());
  }
}

 

 

代码清单 11

try
{
JdbcTemplate jt = new JdbcTemplate(this.myDataSource);
jt.execute("DROP TABLE salgrade PURGE");
}
catch ( BadSqlGrammarException badSqlEx ) // unchecked
{
System.err.println( badSqlEx.getMessage() );
System.err.println( "BadSQL:" + badSqlEx.getSql() );
}

这时应该很明显,基于 Spring 的代码比直接的 JDBC 代码要更易于阅读(以及编写和维护)。实际上,这个代码清单中只有两行是绝对必需的,因为捕获的异常是一个非强制异常。

利用 Spring 框架访问存储过程

代码清单 13 和 14 分别演示了从直接的 JDBC 和 Spring 包装的 JDBC 来访问一个经过设计的存储过程(如代码清单 12 所示)。

代码清单 12
CREATE OR REPLACE PROCEDURE salary_percentile (
salary IN emp.sal%TYPE,
low IN salgrade.losal%TYPE,
high IN salgrade.hisal%TYPE,
percentile OUT NUMBER )
AS
BEGIN
IF salary < 0 THEN
percentile := null;
ELSE
percentile := (salary - low) / (high - low);
END IF;
END;

代码清单 13
String escapeString = "{call salary_percentile (?,?,?,?)}";
CallableStatement cStmt = null;
double percentile = -1.0;
final int PERCENTILE_INDEX = 4;

try
{
cStmt = this.myConnection.prepareCall(escapeString);
cStmt.setInt(1, aSalary); // aSalary passed into this method
cStmt.setInt(2, aLow);   // aLow passed into this method
cStmt.setInt(3, aHigh);  // aHigh passed into this method
cStmt.registerOutParameter(PERCENTILE_INDEX, Types.DOUBLE);
cStmt.execute();
percentile = cStmt.getDouble(PERCENTILE_INDEX);
}
catch ( SQLException sqlEx )
{
System.err.println("Message:" + sqlEx.getMessage());
System.err.println("Error Code:" + sqlEx.getErrorCode());
System.err.println("SQL State:" + sqlEx.getSQLState());
}
finally
{
if ( cStmt != null )
{
try
{
cStmt.close();
}
catch (SQLException finallySqlEx)
{
System.err.println(finallySqlEx.getMessage());
}
}
}

return percentile;

 

代码清单 14 中的使用基于 Spring 的代码来访问存储过程的示例演示了 org.springframework.jdbc.object.StoredProcedure 类的用法。

(包含这个 StoredProcedure 类的 Spring 程序包也包含了除存储过程调用之外的其他类型的 SQL 语句的对象表示。关于其他 SQL 语句类型的对象程序包和对象表示的更多详细信息,请参见 Spring 的文档。)

代码清单 14

private class SalaryCalculator extends StoredProcedure
{
/** Name of procedure in database. */
public static final String PROC_NAME = "salary_percentile";
 
  /**
* Constructor for this StoredProcedure class.
* @param ds Data Source.
  */
public SalaryCalculator(DataSource ds)
  {
setDataSource(ds);
setSql(PROC_NAME);
 
// Parameters should be declared in same order here that
// they are declared in the stored procedure. 
 
declareParameter(new SqlParameter("salary", Types.DOUBLE));
declareParameter(new SqlParameter("low", Types.INTEGER));
declareParameter(new SqlParameter("high", Types.INTEGER));
declareParameter(new SqlOutParameter( "percentile",
Types.DOUBLE ) );
compile();
  }
   
  /**
* Execute stored procedure.
* @return Results of running stored procedure.
  */
public Map executeCalculation( double aSalary,
int aLow,
int aHigh )
  {
Map inParameters = new HashMap();
inParameters.put( "salary", new Double(aSalary) );
inParameters.put( "low", new Integer(aLow) );
inParameters.put( "high", new Integer(aHigh) );
Map out = execute( inParameters ); // Call on parent class
return out;
   }
  }
 
// . . .
// Code below is all that is needed to call your Stored Procedure
// created above. 
// . . .
 
SalaryCalculator calcPercentile =
new SalaryCalculator(this.myDataSource);
Map calcResults =
calcPercentile.executeCalculation(aSalary, aLow, aHigh);
   
return ((Double)calcResults.get("percentile")).doubleValue();
 
// . . .
// remainder of class
// . . .

代码清单 14 中的程序在单个类中表示代码。该代码清单中显示的代码的主体是一个扩展了 Spring 的 StoredProcedure 的内部类 (SalaryCalculator)。

这个由开发人员创建的类包装了代码清单 12 中显示的存储过程。调用 SalaryCalculator 类只需要几行代码。因此,SalaryCalculator 类对调用存储过程所涉及的大部分问题进行了抽象化。 虽然开发人员必须编写扩展 StoredProcedure 的类,但这么做(而不是直接编写存储过程访问)将带来相应的好处。一个好处是能够使用 Spring 的特有的非强制 DAO 和 JDBC 异常而不是通用的强制 SQLException。此外,正如代码代码清单所示,处理关闭资源的烦琐工作被抽象化了。 注意在将小于 0 的薪水值传给存储过程时代码清单 13 和代码清单 14 返回的结果的差异是很有趣的。在直接的 JDBC 的情况下(代码清单 13),将返回值 0.0,而 wasNull() 是确定结果是否真正为空的唯一方式。在基于 Spring 的代码中(代码清单 14),这种情况下将返回一个 Java null,无需 wasNull() 调用。

利用 Spring 访问 Oracle 对象

Spring Framework 的 JDBC 抽象可以与 Oracle 对象(例如在代码清单 15 中创建的对象)结合使用。

代码清单 15
CREATE OR REPLACE TYPE address_type AS OBJECT
(
STREET VARCHAR2(20),
CITY  VARCHAR2(15),
STATE  CHAR(2),
ZIP   CHAR(5)
);
/

CREATE TABLE emp_address_table
(
EMP_NUM NUMBER(4),
ADDRESS ADDRESS_TYPE
);
利用 JDBC 访问 Oracle 对象存在两种常用的方法。一种方法是将标准的 JDBC 接口 java.sql.Struct 与其 Oracle 驱动程序特有的类实施 oracle.sql.STRUCT 结合使用。第二种方法是创建与 Oracle 对象类型映射的 Java 类。将 Oracle 对象与 Oracle JDeveloper 10g IDE 和 JPublisher 结合使用是件轻而易举的事。
使用 java.sql.Struct 方法或 JPublisher 方法的一个有趣的“副作用”:如果您想在这些对象中访问数据,那么必须处理 SQLException。例如,使用 java.sql.Struct 方式,getAttributes() 方法将抛出 SQLException。同样,JDeveloper/JPublisher 创建的 Java 类也将包含抛出 SQLException 的方法。访问这些 Java 对象的开发人员将必须处理这些 SQLException,或者可以使用 Spring(如代码清单 16 和 17 所示)。
代码清单 16
String queryStr =  "SELECT address FROM emp_address_table WHERE "
+ "emp_num = " + aEmpNum; // aEmpNum passed in
final List addresses = new ArrayList();

JdbcTemplate jt = new JdbcTemplate(this.myDataSource);
jt.query( queryStr,
new RowCallbackHandler()
{
public void processRow(ResultSet rs)
throws SQLException
{
// The Struct and ResultSet methods throw
// SQLException, so throws above is necessary
java.sql.Struct address =
(java.sql.Struct) rs.getObject(1);
String street =
address.getAttributes()[0].toString();
String city =
address.getAttributes()[1].toString();
String state =
address.getAttributes()[2].toString();
String zipCode =
address.getAttributes()[3].toString();
String addressStr = street + ", " + city + ", "
+ state + " " + zipCode;
addresses.add( addressStr );
}
}
);

代码清单 17
String updateStr =  "UPDATE emp_address_table SET address = ? "
+ "WHERE emp_num = ?";

JdbcTemplate jt = new JdbcTemplate( getDataSource() );
jt.update( updateStr,
new PreparedStatementSetter()
{
public void setValues(PreparedStatement ps)
throws SQLException
{
Address address = new Address();
address.setStreet(aStreet);
address.setCity(aCity);
address.setState(aState);
address.setZip(aZipCode);

ps.setObject(1, address);
ps.setInt(2, aEmpNum);
}
}
);

在利用 JDBC 访问 Oracle 对象的两种方式下(Struct 和 SQLData)都将使用抛出 SQLException 的方法来访问返回的类。代码清单 16 和 17 显示了如何使用匿名内部回调类来将 SQLException 隐藏在特有的非强制 Spring 框架异常层次结构之后。这些重新抛出的异常利用了与本文中的其他示例相同的异常转换。这些代码清单不仅专门演示了如何处理 Oracle 对象的基于 Spring 的访问,还演示了在其他的 JdbcTemplate 常规方法不适用时如何使用匿名内部回调类。 在代码清单 16 中,您将在本文中第一次看到在基于 Spring 的代码中出现 ResultSet 和 SQLException。不过,注意甚至在这些代码中也没有直接使用 SQLException。相反,Spring 框架通过其异常处理机制来处理任何抛出的 SQLException,您只需关心您希望捕获和处理的 Spring 异常。

在线阅读

JDBC 资源
JDBC/SQLJ 论坛
核心的 J2EE 设计模式 — 数据访问对象 (DAO)
“持久化和 POJO:连接对象和关系”
Oracle TopLink Spring 集成
使用 Oracle JDBC 的 Java 编程,第 19 章,“性能”(示例章节)
最重要的 10 个 Oracle JDBC 提示(摘自 OnJava)
“JPublisher:简化数据库访问”(摘自 Oracle 杂志

代码清单 17 演示了本文中第一次在基于 Spring 的代码中使用 PreparedStatement,并显示了对 SQLException 的另一种引用。

正如代码清单 16 的情况一样,SQLException 主要用于引用 Spring 框架的 JdbcTemplate 类,后者将处理它并将任何异常作为非强制 Spring 异常提供。 代码清单 16 和 17 演示了 Spring 的 RowCallbackHandler 和 PreparedStatementSetter 回调接口的用法。在这些代码清单中使用匿名内部类实现了这些接口。虽然与前面的代码清单中显示的 JdbcTemplate 的更简单的用法相比,开发人员编写的内部类必须知道关于 ResultSet 和 PreparedStatement 以及它们的各个 API 的更多信息,但您仍然无需关心 SQLException 的处理;JdbcTemplate 将执行异常处理。 前面的基于 Spring 的代码清单(例如代码清单 36 中使用的 JdbcTemplate)甚至没有提到 ResultSet、Statement、PreparedStatement 或 SQLException。这些高度抽象的方法对于不想关心 JDBC 的具体用法的开发人员特别有用。不过,这些极其方便的方法没有代码清单 1617 所演示的内部类方法灵活。代码清单 16 和 17 中显示的更灵活的方法可以在需要时使用(只需稍微了解基本的 JDBC API)。在所有情况下,异常处理都由 Spring 异常层次结构来一致地执行,您不需要关心 SQLException。

其他好处

Spring 框架为 JDBC 带来的好处除上面稍微详细介绍的之外,还存在其他几个将 Spring 框架用于 JDBC 的好处(在此不再进一步讨论)。这些好处包括:

  • Spring 的 JdbcTemplate 类支持其他几个由 Spring 提供的与本文所讨论的接口类似的接口。这些接口包括 ResultSetExtractor 和 PreparedStatementCreator,它们分别类似于 RowCallbackHandler 和 PreparedStatementSetter。JdbcTemplate 支持的许多接口中的另一个是非常有用的 BatchPreparedStatementSetter。
  • 虽然不需要在 Spring 应用程序上下文中使用 Spring JDBC 抽象,但可以选择将 JDBC 抽象用于该上下文,这提供了额外的好处。可以使用 Spring 框架通过配置文件来将数据访问对象与业务对象相连(而不是在代码中直接耦合)。
  • org.springframework.jdbc.object 程序包支持将关系数据库操作(包括 DML 语句和存储过程)作为可重用的对象。这些线程安全的对象为开发人员提供了更高级别的关系数据库抽象(超过本文之前介绍和在所有代码示例中使用的 Spring 的 JdbcTemplate)。我个人喜欢 JdbcTemplate 提供的抽象级别,我觉得它对例行问题(如异常、结果集和资源管理)的处理所提供的支持级别正是我想要的。不过,如果机构或其开发人员想享受更高级别的关系数据库概念的抽象,而不必采用完整的对象-关系 (O/R) 映射技术(例如 TopLink 或 Hibernate),那么 org.springframework.jdbc.object 程序包将值得考虑。代码清单 12 中的存储过程示例提供了对该程序包的简单尝试。
  • Spring 框架提供了一个可以在容器外部使用的简单的 DataSource 实现,并提供了一个可以根据需要覆盖的抽象数据源类(参见 Spring 参考文档的第 10 章)。
  • 除 JDBC 之外,Spring 框架还支持多种 O/R 映射技术(例如 Hibernate、iBatis 和 Java 数据对象 (JDO))。(参见 Spring 参考文档的第 11 章。)本文底部的在线参考部分包含了一条到关于将 Spring 与 Oracle 的 TopLink 对象-关系映射产品结合使用的其他文章的链接。
  • Spring 框架简化了共享相同的异常层次结构的技术不可知的 DAO 接口和实现的开发。(参见 Spring 参考文档的第 9 章。)本文中的几个地方简单提到了这个主题。
  • Spring 框架提供了对在需要时将 SQLWarning 作为异常 (SQLWarningException) 来捕获的支持。这对 Oracle 数据库和 JDBC 驱动程序不是特别有用,因为它们倾向于对几乎每一种情况抛出 SQLException。它对于需要提供警告并在使用一个更密集使用 SQLWarning 的数据库的开发人员而言是一种更有用的特性。在这些情况下,直接使用 JDBC 的开发人员必须特别询问是否出现了 SQLWarning。Spring 框架可以用来处理这些警告(和处理任何其他的 Spring 异常一样),以实现更容易和更一致的处理。
  • Spring 框架提供了 org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor 接口和该接口的一些实现(例如 SimpleNativeJdbcExtractor)。当连接被另一个 DataSource 包装或者要通过特定的连接池来获取时,这些接口和实现对于通过 Oracle 连接或 ResultSet 来访问 Oracle 特性非常有用。
  • Spring 提供了 org.springframework.jdbc.support.lob.OracleLobHandler 类来创建 oracle.sql.BLOB(二进制大对象)和 oracle.sql.CLOB 的实例。
  • Spring 提供的 OracleSequenceMaxValueIncrementer 类提供了 Oracle 序列的下一个值。它可以有效地提供一些信息,这些信息与您直接使用以下命令时提供的信息相同:select someSequence.nextval from dual (其中 someSequence 是 Oracle 数据库中的序列名称)。该方法的一个好处是可以在 DAO 层次结构中使用 DataFieldMaxValueIncrementer 接口,而无需紧密地耦合 Oracle 特有的实现。

Spring 框架的其他用途 本文专注于使用 Spring 来编写更易于维护和更不易出错的 JDBC 代码。

Spring 框架还可以用来做比这更多得多的事情。除了能够为 JDBC 做和许多对象-关系映射技术所能做的同样的事情之外,Spring 还拥有支持可以在企业应用程序中找到的所有其他层的特性。这些特性包括轻量级的 IoC(反转控制)容器支持、一个 Web 框架和其他的 J2EE 应用程序支持。 Spring 框架被设计为非侵入性或者最小限度的侵入性。您可以使用 Spring 框架来做许多事情,但并不强迫应用程序全依赖于 Spring 框架。因为 Spring 是开放源代码的,是一个很有用的工具,可作为优秀应用程序设计准则的具体示例。即使目前您不能直接在应用程序中使用 Spring 框架,学习使用 Spring 框架作为优秀设计和代码准则的示例也是很有收获的。Spring 提供的 JDBC 支持不需要使用 Spring 的任何其他特性,是极佳的 Spring 框架使用入门途径,它不用与 Spring 的其他部分紧密结合。


Dustin Marx 是 Raytheon Co. 的高级软件工程师和架构师。

标签 : , ,



发表评论 发送引用通报