自从核心 java语言的第一个公开发行版本起,JDBC( java数据库连接)已经经历了十年的发展历程。它的当前版本4.0(将与 java标准版本6.0一起打包发行)提供了一组更为丰富的API,主要目的在于改进 软件开发的设计和性能。
本文将重点讨论JDBC 4.0规范在设计和性能方面的改进。
一、 注释和泛型DataSet
在本文中,我假定你已经了解注释和泛型。其实,这两个概念是随着 J2SE 5.0的发行一同引入的;与此同时,JDBC 4.0中引入了注释和泛型DataSet。这一变化的主要目的是为了简化SQL查询和SQL DML(数据操纵语言)语句的执行。
新一代API定义了一组Query和DataSet接口。这个Query接口定义了一组带有JDBC注释的方法。这些带有注释的方法描述了SQL select和update语句,并且指定应该如何把结果绑定到一个DataSet上。这个DataSet接口是一个通过泛型定义实现的参数化类型,也为结果集数据提供一种类型安全的定义。
所有的Query接口都继承自BaseQuery接口。你可以使用Connection.createQueryObject()或DataSource.createQueryObject()方法(这两个方法都使用一个Query接口类型作为它的参数)来具体实现这样的接口。
一个DataSet接口继承自 java.util.List;该接口使用一个描述结果集数据列的数据库(该数据库是通过Query接口的一个注释的方法返回的)作为它的参数类型。在连接方式和非连接方式下都可以操作和使用DataSet。因此,根据使用的是连接方式还是非连接方式,这个DataSet也分别相应地实现为一个ResultSet或CachedRowSet。DataSet,作为 java.util.List的一个子接口,允许使用Iterator模式通过 java.util.Iterator接口存取其数据行。
你可以用两种方式来指定数据类或用户定义类(作为DataSet接口的一个参数类型)-作为一个结构或作为一个 javaBeans对象。无论哪一种方式都能够把结果集数据列绑定到用户定义的类定义上;但是, javaBeans组件模型更漂亮些,并且更利于对象定义在另外的支持 javaBeans模型的框架中的重用。
列表1摘自本文示例中的代码片断,它展示了如何使用这种新型的API来创建和运行SQL查询:使用一个用户定义类定义结果集数据,并且把返回的结果集绑定到用户定义的描述中。
列表1.Employee用户定义类型与employeeQueries接口
pubic class Employee { private int employeeId; private String firstName; private String lastName; public int getEmployeeId() { return employeeId; } public setEmployeeId(int employeeId) { this.employeeId = employeeId; } public String getFirstName() { return firstName; } public setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public setLastName(String lastName) { this.lastName = lastName; } } interface EmployeeQueries extends BaseQuery { @Select (sql="SELECT employeeId, firstName, lastName FROM employee") DataSet<Employee> getAllEmployees (); @Update (sql="delete from employee") int deleteAllEmployees (); } Connection con = ... EmployeeQueries empQueries = con.createQueryObject (EmployeeQueries.class); DataSet<Employee> empData = empQueries.getAllEmployees (); |
二、 改进异常处理能力
在JDBC API 4.0以前的版本中,异常处理功能极其有限。对于所有类型的错误都会笼统地抛出一个SQLException异常-根本不存在异常的详细分类,且没有相应的层次定义。所以这时,你唯一能够得到一些有意义的信息的办法是检索和分析SQLState值。另一方面,SQLState值及其相应的含义会因不同的数据源而有所改变;因此,要想追踪到问题的"根部"并且有效地处理异常是一件非常乏味的任务。
JDBC 4.0改进了异常处理能力,同时也缓解了一些前面提到的问题。其中的关键改进有:
· 把SQLException分成短暂异常和非短暂异常两种类型
· 支持链式异常
· 实现Iterable接口
当一个以前失败的操作检索成功时,将会抛出SQLTransientException异常;而在检索不成功时将会抛出SQLNonTransientException异常-除非导致SQLException的原因得到纠正。
图1展示了子类SQLTransientException和SQLNonTransientException。
图1.SQL异常类型:短暂型和非短暂型 |
另一方面,在新的API中,加入了对链式异常的支持。新的异常基类构造器中添加了额外参数以捕获异常的可能原因。例如,在一个循环中可能存在SQLException遍历;这时,开发人员可以调用getCause()来决定异常的可能原因。如果获取的结果的确是产生这些异常的原因,那么getCause()方法能够返回一个非SQLException。
现在,SQLException类实现了Iterable接口并且支持J2SE 5.0的for each循环。
列表2描述了新的for-each-loop结构的用法:
列表2.For each循环结构
catch(SQLException ex) { for(Throwable t : ex) { System.out.println("exception:" + t); } } |
三、 支持XML数据类型
如今,大量的数据行以XML格式存在。通过在SQL 2003规范中定义了一种标准XML类型,现在大多数数据库都已提供对XML数据类型的支持。通过加入这样一种数据类型,一个XML数据集或文档可能成为一个数据库表中的一行的一个字段或列值。在JDBC 4.0以前,也许在JDBC框架内操作这样的数据的最好的方法是使用来自于驱动程序供应商的专利扩展产品或作为一种CLOB类型来存取它。
现在,JDBC 4.0把SQLXML定义为映射数据库SQL XML类型的Java数据类型。这种API支持把一个XML类型作为一个字符串或作为一个StAX流进行处理。Streaming API for XML(在JSR 173规范中确立)基于Iterator模式,它与基于Observer模式的Simple API for XML Processing(SAX)形成对照。
调用Connection对象的createSQLXML()方法就能够创建一个SQLXML对象。开始时这是一个空对象;因此,通过使用setString()方法或createXMLStreamWriter()方法把一个XML流关联到该对象可以把数据依附到其上。同样,XML数据可以从一个SQLXML对象中进行检索,这是通过使用getString()或createXMLStreamReader()方法把一个XML流与该对象关联实现的。
ResultSet,PreparedStatement和CallableStatement接口中都提供了getSQLXML()方法用于检索SQLXML数据类型。另外,PreparedStatement和CallableStatement中还引入了setSQLXML()方法用于把SQLXML对象作为参数添加。
当对象在长时间运行的事务中保持有效时,可以通过调用上面这些接口的free()方法来释放SQLXML资源;事实证明这是一种比较适当的方法。另外,开发者可以在一个数据源上调用DatabaseMetaData的getTypeInfo()方法来检查数据库是否支持SQLXML数据类型,因为这个方法能够返回它支持的所有数据类型。
四、 改进Connection接口
Connection接口定义也得到了增强,用于更高效地分析连接状态。
有时数据库连接是不可用的,尽管可能不必关闭这些连接并对之进行垃圾回收。处于这样的情况下,数据库常常表现出速度缓慢且不具有响应性。此时,在大多数情况下,重新初始化该连接也许是解决这种问题的唯一方法。在JDBC 4.0以前版本时,没有办法来区分一个旧连接和一个已经关闭的连接;而新式API则在Connection接口中添加了一个isValid()方法用来查询是否连接仍然有效。
另外,数据库连接经常在客户端被共享;并且有时,一些客户使用的资源比另一些客户多,这可能会导致一种"饥饿"现象。为此,Connection接口中定义了一个setClientInfo()方法以定义客户端特定的属性,这可以被客户端用于分析和监控资源利用情况。
五、 有关RowId方面的改进
在许多数据库中,RowId都被用作唯一标识一个表中行的方法。在查询条件中使用RowId往往是检索数据的最快方法,特别是在Oracle和DB2数据库情况下。现在,既然Java.sql.RowId是一种内嵌的java类型;那么,你就可以充分利用与其用法相关的性能优点。当表中存在重复的数据并且一些行数据相同时,RowId是标识唯一行的最有效的方法。然而,还要注意到,RowId在一个表中是唯一的,而对于整个数据库来说并非如此;它们可能发生变化并且不为所有数据库所支持。典型情况下,RowId不是跨数据源可移植的;因此,当使用多种数据源时应该慎重。
在数据源定义的生命周期内,只要一行未被删除,那么该行相应的RowId就一直保持有效。我们可以调用DatabaseMetadata.getRowIdLifetime()方法来决定RowId的生命周期。这个方法的返回类型是一个枚举类型。现在,把所有这些枚举类型总结到如下的表格中。
RowIdLifetime枚举类型 | 定义 |
ROWID_UNSUPPORTED | 数据源不支持RowId类型 |
ROWID_VALID_OTHER | 实现依赖的生命周期 |
ROWID_VALID_TRANSACTION | 生命周期至少包含事务 |
ROWID_VALID_SESSION | 生命周期至少包含会话 |
ROWID_VALID_FOREVER | 无限制生命周期 |
其中,只要没有删除行,那么ROWID_VALID_TRANSACTION,ROWID_VALID_SESSION和ROWID_VALID_FOREVER都定义为true。还要注意的是,如果一个行被删除和重新插入,那么RowId会被重新调整(这有可能在数据源中透明实现)。作为一个例子,在Oracle中,如果在一个分区表上设置"enable row movement"语句,并且分区键的一个更新导致该行从一个分区移动到另一个分区,那么RowId将改变。即使在没有设置"enable row movement"标志并且"alter table table_name"发生改变时,RowId也能够改变。
ResultSet和CallableStatement接口都被更新-都包括了一个返回 javax.sql.RowId类型的方法getRowID()。
列表3展示了如何从一个ResultSet和CallableStatement中检索RowId。
列表3.得到RowId
//从一个ResultSet检索RowId的方法签名: RowId getRowId (int columnIndex) RowId getRowId (String columnName) ... Statement stmt = con.createStatement (); ResultSet rs = stmt. ExecuteQuery (…); while (rs.next ()) { ... java.sql.RowId rid = rs.getRowId (1); ... } //从一个CallableStatement检索RowId的方法签名: RowId getRowId (int parameterIndex) RowId getRowId (String parameterName) Connection con; ... CallableStatement cstmt = con.prepareCall (…); ... cstmt.reGISterOutParameter (2, Types.ROWID); ... cstmt.executeUpdate (); ... java.sql.RowId rid = cstmt.getRowId (2); |
在此,RowId可以用于唯一地参考一行并因此可被用于检索或更新行数据。当使用RowId参考来存取或更新数据时,理解生命周期的有效性是十分重要的,从而保证结果的连续性。另外,我还建议你同时使用另一个参考,例如主键,以避免在能够透明地改变RowId的情况下出现不连续的结果。
RowId值还可以被设置或更新。在一种可更新的ResultSet情况下,可以针对表中的一个特定的行使用updateRowId()方法来更新RowId。
另外,PreparedStatement和CallableStatement接口都支持setRowId()方法(其形式不一样),该方法把RowId设置为一个参数值。这个值可以用于针对表中的一个特定的行来参考数据行或更新RowId值。
由上面可知,开发者可以非常容易地设置或更新RowId;这为控制唯一的行标识符并为使这些标识符具有跨表唯一性提供了极大的灵活性。也许,跨表支持的数据源的RowId的可移植性还能够通过在这些数据源间显式地设置一致值来实现。然而,因为系统生成的RowId经常是有效的,并且可以通过透明的操作来改变RowId;所以,最好在一个应用程序中把它们用作只读属性。
六、 利用非标准供应商实现的资源
新型的 JDBC API中定义了一个 java.sql.Wrapper接口。通过检索代理实例并使用相应的包装代理实例,这个接口提供了存取数据源供应商特定资源的能力。
这个包装接口拥有17个子接口,并且包括Connection,ResultSet,Statement,CallableStatement,PreparedStatement,DataSource,DatabaseMetaData和ResultSetMetaData,等等。这是一种优秀的设计,因为它方便了在创建查询和"结果-设置-检索"生命周期的几乎每一个阶段使用数据源供应商特定的资源。
unwrap()方法返回实现给定接口的对象,从而允许存取供应商特定的方法。isWrapperFor()方法返回一个Boolean值-如果它实现了该接口则返回true;或者,它也有可能直接或间接地成为对象的一个包装类。
作为一个例子,当使用Oracle时,Oracle JDBC 驱动程序提供了更新批扩展-与标准JDBC批更新机制相比,它具有更好的性能且更为有效。对于早期的JDBC版本来说,这意味着要在代码中使用Oracle特定的定义,例如OraclePreparedStatement。这样以来就减弱了代码的可移植性。而借助于现在新型的API,许多前面这些有效的实现都能够被包装和被暴露在标准JDBC定义中。
七、 针对驱动程序加载的服务提供者机制
在JDBC 4.0以前,在一种非托管的或独立的程序中,你必须显式地通过调用Class.forName方法来加载JDBC驱动器类,如列表4所示:
列表4.Class.forName方法
Class.forName ("com.drivERProvider.jdbc.jdbcDriverImpl"); |
借助于JDBC 4.0,如果JDBC驱动程序供应商把他们的驱动程序打包为服务(在服务提供者机制下定义为每一种JAR规范),那么DriverManager代码将通过在classpath中搜索它来隐式地装载该驱动程序。这种机制的优点在于,开发者不需要了解这种特定的驱动程序类,并且能够使用JDBC来编写较少的代码实现。另外,既然驱动程序类名不再存在于代码中,那么只改变一个名字并不要求重新编译。如果在classpath中指定了多个驱动程序,那么DriverManger将试图使用它在classpath中所找到的第一个驱动程序来创建一种连接,并且在需要时能够继续遍历下一个驱动程序。
八、 结论
在本文中,我们一同探讨了JDBC 4.0的一些新的和改进的特征。从中可以看出,许多新特征进一步便利了开发,从而提高了开发者的生产效率。另一方面,该规范并没有消除对于其它JDBC框架提供的模板化工具和高级异常处理能力的使用。然而,对该规范也存在一些批评。例如,一些人认为注释的使用会导致在代码中硬编码(而这往往导致在代码维护阶段出现问题)。究其实效,还有待于实践检验。