Jade Dungeon

JDBC 4

注册JDBC驱动

在 JDBC 4.0 之前,编写 JDBC 程序都需要加上以下这句有点丑陋的代码:

Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance();

Java.sql.DriverManager的内部实现机制决定了这样代码的出现。 只有先通过Class.forName找到特定驱动的 class 文件,DriverManager.getConnection 方法才能顺利地获得 Java 应用和数据库的连接。

这样的代码为编写程序增加了不必要的负担,JDK 的开发者也意识到了这一点。 从 Java 6 开始,应用程序不再需要显式地加载驱动程序了, DriverManager 开始能够自动地承担这项任务。

这就要归功于一种被称为 Service Provider 的新机制。 熟悉 Java 安全编程的程序员可能对其已经是司空见惯, 而它现在又出现在 JDBC 模块中。JDBC 4.0 的规范规定, 所有 JDBC 4.0 的驱动 jar 文件必须包含一个java.sql.Driver, 它位于 jar 文件的META-INF/services目录下。 这个文件里每一行便描述了一个对应的驱动类。

其实,编写这个文件的方式和编写一个只有关键字(key)而没有值(value)的 properties文件类似。同样地,#之后的文字被认为是注释。有了这样的描述, DriverManager 就可以从当前在CLASSPATH中的驱动文件中找到,它应该去加载哪些类。

而如果我们在CLASSPATH里没有任何 JDBC 4.0 的驱动文件的情况下, 调用以下代码会输出一个sun.jdbc.odbc.JdbcOdbcDriver类型的对象。

// 罗列本地机器上的 JDBC 驱动
Enumeration<Driver> drivers = DriverManager.getDrivers();

while(drivers.hasMoreElements()) {
    System.out.println(drivers.nextElement());
}

而仔细浏览 JDK 6 的目录,这个类型正是在$JAVA_HOME/jre/lib/resources.jarMETA-INF/services目录下的java.sql.Driver文件中描述的。也就是说, 这是 JDK 中默认的驱动。而如果开发人员想使得自己的驱动也能够被 DriverManager 找到, 只需要将对应的 jar 文件加入到CLASSPATH中就可以了。

当然,对于那些 JDBC 4.0 之前的驱动文件,我们还是只能显式地去加载了。

RowId

熟悉 DB2、Oracle 等大型 DBMS 的人一定不会对 ROWID 这个概念陌生:

它是数据表中一个「隐藏」的列,是每一行独一无二的标识,表明这一行的物理或者逻辑位置。 由于ROWID类型的广泛使用,Java SE 6 中新增了java.sql.RowId的数据类型, 允许 JDBC 程序能够访问 SQL 中的 ROWID 类型。

诚然,不是所有的 DBMS 都支持 ROWID 类型。即使支持,不同的 ROWID 也会有不同的生命周期。 因此使用DatabaseMetaData.getRowIdLifetime来判断类型的生命周期不失为一项良好的实践经验。

我们在下代码可以了解 ROWID 类型的支持情况:

// 了解 ROWID 类型的支持情况
DatabaseMetaData meta = conn.getMetaData();
System.out.println(meta.getRowIdLifetime());

Java SE 6 的 API 规范中,java.sql.RowIdLifetime规定了5种不同的生命周期:

  • ROWID_UNSUPPORTED
  • ROWID_VALID_FOREVER
  • ROWID_VALID_OTHER
  • ROWID_VALID_SESSION
  • ROWID_VALID_TRANSACTION

从字面上不难理解它们表示了不支持 ROWID、ROWID 永远有效等等。具体的信息, 还可以参看相关的 JavaDoc。

读者可以尝试着连接 Derby 进行试验,会发现运行结果是ROWID_UNSUPPORTED, 即 Derby 并不支持 ROWID。

既然提供了新的数据类型,那么一些相应的获取、更新数据表内容的新 API 也在 Java 6 中被添加进来。和 其它已有的类型一样,在得到ResultSet或者CallableStatement之后, 调用get/set/update方法得到/设置/更新 RowId 对象,示例的代码:

// Initialize a PreparedStatement  
PreparedStatement pstmt = connection.prepareStatement(  
	"SELECT rowid, name, score FROM hellotable WHERE rowid = ?");  

// Bind rowid into prepared statement.   
pstmt.setRowId(1, rowid);  

// Execute the statement  
ResultSet rset = pstmt.executeQuery();   

// List the records  
while(rs.next()) {
	RowId id = rs.getRowId(1); // get the immutable rowid object  
	String name = rs.getString(2);  
	int score = rs.getInt(3);  
}  

鉴于不同 DBMS 的不同实现,RowID 对象通常在不同的数据源(datasource)之间并不是可移植的。 因此 JDBC 4.0 的 API 规范并不建议从连接 A 取出一个 RowID 对象,将它用在连接 B 中, 以避免不同系统的差异而带来的难以解释的错误。

而至于像 Derby 这样不支持 RowId 的 DBMS,程序将直接在setRowId方法处抛出SQLFeatureNotSupportedException

SQLXML

SQL:2003 标准引入了 SQL/XML,作为 SQL 标准的扩展。SQL/XML 定义了 SQL 语言怎样和 XML 交互: 如何创建 XML 数据;如何在 SQL 语句中嵌入 XQuery 表达式等等。

作为 JDBC 4.0 的一部分,Java 6 增加了java.sql.SQLXML的类型。 JDBC 应用程序可以利用该类型初始化、读取、存储 XML 数据。 java.sql.Connection.createSQLXML方法就可以创建一个空白的 SQLXML 对象。 当获得这个对象之后,便可以利用setStringsetBinaryStreamsetCharacterStream 或者setResult等方法来初始化所表示的 XML 数据。

setCharacterStream为例,以下代码表示了一个SQLXML对象如何获取java.io.Writer对象, 从外部的 XML 文件中逐行读取内容,从而完成初始化。

SQLXML xml = con.createSQLXML();  
Writer writer = xml.setCharacterStream();  
BufferedReader reader = new BufferedReader(new FileReader("test.xml"));  
String line= null;  
while((line = reader.readLine() != null) {  
	writer.write(line);  
}  

由于 SQLXML 对象有可能与各种外部的资源有联系,并且在一个事务中一直持有这些资源。 为了防止应用程序耗尽资源,Java 6 提供了free方法来释放其资源。 类似的设计在java.sql.ArrayClob中都有出现。

至于如何使用SQLXML与数据库进行交互,其方法与其它的类型都十分相似。 可以参照 RowId 一节 中的例子在 Java SE 6 的 API 规范中找到 SQLXML 中对应的get/set/update方法构建类似的程序,此处不再赘述。

SQLExcpetion 的增强

在 Java SE 6 之前,有关 JDBC 的异常类型不超过 10 个。 这似乎已经不足以描述日渐复杂的数据库异常情况。 因此,Java SE 6 的设计人员对以java.sql.SQLException为根的异常体系作了大幅度的改进。 首先,SQLException 新实现了Iterable<Throwable>接口。

除去原有的SQLException的子类,Java 6 中新增的异常类被分为 3 种:

  • SQLReoverableException
  • SQLNonTransientException
  • SQLTransientException

SQLNonTransientExceptionSQLTransientException之下还有若干子类, 详细地区分了 JDBC 程序中可能出现的各种错误情况。大多数子类都会有对应的标准SQLState值, 很好地将 SQL 标准和 Java 6 类库结合在一起。

在众多的异常类中,比较常见的有SQLFeatureNotSupportedException, 用来表示 JDBC 驱动不支持某项 JDBC 的特性。例如在 Derby 下并不支持 RowId 的特性。

另外值得一提的是,SQLClientInfoException直接继承自SQLException, 表示当一些客户端的属性不能被设置在一个数据库连接时所发生的异常。

以下例子演示了异常处理机制。这样简洁地遍历了每一个SQLException和它潜在的原因(cause)。

catch (Throwable e) {  
	if (e instanceof SQLException) {  
		for (Throwable ex : (SQLException) e) {
			System.err.println(ex.toString());
		}  
	}  
}  

其他特性

其他还有增加了对 SQL 语言中NCHARNVARCHARLONGNVARCHARNCLOB类型的支持; 在数据库连接池的环境下为管理Statement对象提供更多灵活、便利的方法等。