曹 若沈 (ruoshen_c@sjtu.edu.cn), 上海交通大學電腦系研究生
2007 年 8 月 31 日
2006 年底,Sun 公司發布了 Java Standard Edition 6(Java SE 6)的最終正式版,代號 Mustang(野馬)。跟 Tiger(Java SE 5)相比,Mustang 在效能方面有了不錯的提升。與 Tiger 在 API 庫方面的大幅度加強相比,雖然 Mustang 在 API 庫方面的新特性顯得不太多,但是也提供了許多實用和方便的功能:在指令碼,WebService,XML,編譯器 API,資料庫,JMX,網路和 Instrumentation 方面都有不錯的新特性和功能加強。 本系列 文章主要介紹 Java SE 6 在 API 庫方面的部分新特性,通過一些例子和講解,協助開發人員在編程實踐當中更好的運用 Java SE 6,提高開發效率。
本文是系列文章的第 5 篇,介紹了 Java SE 6 在資料庫編程方面的新特性。
長久以來,由於大量(甚至幾乎所有)的 Java 應用都依賴於資料庫,如何使用 Java 語言高效、可靠、簡潔地訪問資料庫一直是程式員們津津樂道的話題。新發布的 Java SE 6 也在這方面更上層樓,為編程人員提供了許多好用的新特性。其中最顯著的,莫過於 Java SE 6 擁有了一個內嵌的 100% 用 Java 語言編寫的資料庫系統。並且,Java 6 開始支援 JDBC 4.0 的一系列新功能和屬性。這樣,Java SE 在對持久資料的訪問上就顯得更為易用和強大了。
Java DB:Java 6 裡的資料庫
新安裝了 JDK 6 的程式員們也許會發現,除了傳統的 bin、jre 等目錄,JDK 6 新增了一個名為 db 的目錄。這便是 Java 6 的新成員:Java DB。這是一個純 Java 實現、開源的資料庫管理系統(DBMS),源於 Apache 軟體基金會(ASF)名下的項目 Derby。它只有 2MB 大小,對比動輒上 G 的資料庫來說可謂袖珍。但這並不妨礙 Derby 功能齊備,支援幾乎大部分的資料庫應用所需要的特性。更難能可貴的是,依託於 ASF 強大的社區力量,Derby 得到了包括 IBM 和 Sun 等大公司以及全世界優秀程式員們的支援。這也難怪 Sun 公司會選擇其 10.2.2 版本納入到 JDK 6 中,作為內嵌的資料庫。這就好像為 JDK 注入了一股全新的活力:Java 程式員不再需要耗費大量精力安裝和設定資料庫,就能進行安全、易用、標準、並且免費的資料庫編程。在這一章中,我們將初窺 Java DB 的世界,來探究如何使用它編寫出功能豐富的程式。
Hello, Java DB:內嵌模式的 Derby
既然有了內嵌(embedded)的資料庫,就讓我們從一個簡單的範例(代碼在 清單 1 中列出)開始,試著使用它吧。這個程式做了大多數資料庫應用都可能會做的操作:在 DBMS 中建立了一個名為 helloDB 的資料庫;建立了一張資料表,取名為 hellotable;向表插入入了兩條資料;然後,查詢資料並將結果列印在控制台上;最後,刪除表和資料庫,釋放資源。
清單 1. HelloJavaDB 的代碼
public class HelloJavaDB { public static void main(String[] args) { try { // load the driver Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance(); System.out.println("Load the embedded driver"); Connection conn = null; Properties props = new Properties(); props.put("user", "user1"); props.put("password", "user1"); //create and connect the database named helloDB conn=DriverManager.getConnection("jdbc:derby:helloDB;create=true", props); System.out.println("create and connect to helloDB"); conn.setAutoCommit(false); // create a table and insert two records Statement s = conn.createStatement(); s.execute("create table hellotable(name varchar(40), score int)"); System.out.println("Created table hellotable"); s.execute("insert into hellotable values('Ruth Cao', 86)"); s.execute("insert into hellotable values ('Flora Shi', 92)"); // list the two records ResultSet rs = s.executeQuery( "SELECT name, score FROM hellotable ORDER BY score"); System.out.println("name/t/tscore"); while(rs.next()) { StringBuilder builder = new StringBuilder(rs.getString(1)); builder.append("/t"); builder.append(rs.getInt(2)); System.out.println(builder.toString()); } // delete the table s.execute("drop table hellotable"); System.out.println("Dropped table hellotable"); rs.close(); s.close(); System.out.println("Closed result set and statement"); conn.commit(); conn.close(); System.out.println("Committed transaction and closed connection"); try { // perform a clean shutdown DriverManager.getConnection("jdbc:derby:;shutdown=true"); } catch (SQLException se) { System.out.println("Database shut down normally"); } } catch (Throwable e) { // handle the exception } System.out.println("SimpleApp finished"); }} |
隨後,我們在命令列(本例為 Windows 平台,當然,其它系統下稍作改動即可)下鍵入以下命令:
清單 2. 運行 HelloJavaDB 命令
java –cp .;%JAVA_HOME%/db/lib/derby.jar HelloJavaDB |
程式將會按照我們預想的那樣執行,圖 1 是執行結果的一部分截屏:
圖 1. HelloJavaDB 程式的執行結果
上述的程式和以往沒什麼區別。不同的是我們不需要再為 DBMS 的配置而勞神,因為 Derby 已經自動地在目前的目錄下建立了一個名為 helloDB 的目錄,來物理地儲存資料和日誌。需要做的只是注意命名問題:在內嵌模式下驅動的名字應為 org.apache.derby.jdbc.EmbeddedDriver
;建立一個新資料庫時需要在協議後加入 create=true
。另外,關閉所有資料庫以及 Derby 的引擎可以使用以下代碼:
清單 3. 關閉所有資料庫及 Derby 引擎
DriverManager.getConnection("jdbc:derby:;shutdown=true"); |
如果只想關閉一個資料庫,那麼則可以調用:
清單 4. 關閉一個資料庫
DriverManager.getConnection("jdbc:derby:helloDB;shutdown=true "); |
這樣,使用嵌入模式的 Derby 維護和管理資料庫的成本接近於 0。這對於希望專心寫代碼的人來說不失為一個好訊息。然而有人不禁要問:既然有了內嵌模式,為什麼大多數的 DBMS 都沒有採取這樣的模式呢?不妨做一個小實驗。當我們同時在兩個命令列視窗下運行 HelloJavaDB 程式。結果一個的結果與剛才一致,而另一個卻出現了錯誤,如 圖 2 所示。
圖 2. 內嵌模式的局限
錯誤的原因其實很簡單:在使用內嵌模式時,Derby 本身並不會在一個獨立的進程中,而是和應用程式一起在同一個 JAVA 虛擬機器(JVM)裡運行。因此,Derby 如同應用所使用的其它 jar 檔案一樣變成了應用的一部分。這就不難理解為什麼在 classpath 中加入 derby 的 jar 檔案,我們的樣本程式就能夠順利運行了。這也說明了只有一個 JVM 能夠啟動資料庫:而兩個跑在不同 JVM 執行個體裡的應用自然就不能夠訪問同一個資料庫了。
鑒於上述的局限性,和來自不同 JVM 的多個串連想訪問一個資料庫的需求,下一節將介紹 Derby 的另一種模式:網路伺服器(Network Server)。
網路伺服器模式
如上所述,網路伺服器模式是一種更為傳統的用戶端/伺服器模式。我們需要啟動一個 Derby 的網路伺服器用於處理用戶端的請求,不論這些請求是來自同一個 JVM 執行個體,還是來自於網路上的另一台機器。同時,用戶端使用 DRDA(Distributed Relational Database Architecture)協議串連到伺服器端。這是一個由 The Open Group 倡導的資料庫互動標準。圖 3 說明了該模式的大體結構。
由於 Derby 的開發人員們努力使得網路伺服器模式與內嵌模式之間的差異變小,使得我們只需簡單地修改 清單 1 中的程式就可以實現。如 清單 5所示,我們在 HelloJavaDB 中增添了一個新的函數和一些字串變數。不難看出,新的代碼只是將一些在 上一節中特別指出的字串進行了更改:驅動類為 org.apache.derby.jdbc.ClientDriver
,而串連資料庫的協議則變成了 jdbc:derby://localhost:1527/
。這是一個類似 URL 的字串,而事實上,Derby 網路的用戶端的串連格式為:jdbc:derby://server[:port]/databaseName[;attributeKey=value]
。在這個例子中,我們使用了最簡單的本地機器作為伺服器,而連接埠則是 Derby 預設的 1527 連接埠。
圖 3. Derby 網路伺服器模式架構
清單 5. 網路伺服器模式下的 HelloJavaDB
public class HelloJavaDB { public static String driver = "org.apache.derby.jdbc.EmbeddedDriver"; public static String protocol = "jdbc:derby:"; public static void main(String[] args) { // same as before } private static void parseArguments(String[] args) { if (args.length == 0 || args.length > 1) { return; } if (args[0].equalsIgnoreCase("derbyclient")) { framework = "derbyclient"; driver = "org.apache.derby.jdbc.ClientDriver"; protocol = "jdbc:derby://localhost:1527/"; } }} |
當然,僅僅有用戶端是不夠的,我們還需要啟動網路伺服器。Derby 中控制網路伺服器的類是 org.apache.derby.drda.NetworkServerControl
,因此鍵入以下命令即可。如果想瞭解 NetworkServerControl 更多的選項,只要把 start
參數去掉就可以看到協助資訊了。關於網路伺服器端的實現,都被 Derby 包含在 derbynet.jar 裡。
清單 6. 啟動網路伺服器
java -cp .;"C:/Program Files/Java/jdk1.6.0/db/lib/derby.jar";"C:/Program Files/Java/jdk1.6.0/db/lib/derbynet.jar" org.apache.derby.drda.NetworkServerControl start |
相對應的,網路用戶端的實現被包含在 derbyclient.jar 中。所以,只需要在 classpath 中加入該 jar 檔案,修改後的用戶端就可以順利地讀取資料了。再一次嘗試著使用兩個命令列視窗去串連資料庫,就能夠得到正確的結果了。如果不再需要伺服器,那麼使用 NetworkServerControl 的 shutdown 參數就能夠關閉伺服器。
更多
至此,文章介紹了 Java SE 6 中的新成員:Java DB(Derby),也介紹了如何在內嵌模式以及網路伺服器模式下使用 Java DB。當然這隻是淺嘗輒止,更多進階的選項還需要在 Sun 和 Derby 的文檔中尋找。在這一章的最後,我們將簡單介紹幾個 Java DB 的小工具來加快開發速度。它們都位於 org.apache.derby.tools 包內,在開發過程中需要擷取資訊或者測試可以用到。
- ij:一個用來運行 SQL 指令碼的工具;
- dblook:為 Derby 資料庫作模式提取(Schema extraction),產生 DDL 的工具;
- sysinfo:顯示系統以及 Derby 資訊的工具類;
JDBC 4.0:新功能,新 API
如果說上一章介紹了 Java 6 中的一個新成員,它本來就存在,但是沒有被加入進 JDK。那麼這一章,我們將關注在 JDBC 4.0 中又增加了哪些新功能以及與之相對應的新 API。
自動載入驅動
在 JDBC 4.0 之前,編寫 JDBC 程式都需要加上以下這句有點醜陋的代碼:
清單 7. 註冊 JDBC 驅動
Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance(); |
Java.sql.DriverManager
的內部實現機制決定了這樣代碼的出現。只有先通過 Class.forName
找到特定驅動的 class 檔案,DriverManager.getConnection
方法才能順利地獲得 Java 應用和資料庫的串連。這樣的代碼為編寫程式增加了不必要的負擔,JDK 的開發人員也意識到了這一點。從 Java 6 開始,應用程式不再需要顯式地載入驅動程式了,DriverManager 開始能夠自動地承擔這項任務。作為實驗,我們可以將 清單 1 中的相關代碼刪除,重新編譯後在 JRE 6.0 下運行,結果和原先的程式一樣。
好奇的讀者也許會問,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 的驅動檔案的情況下,調用 清單 8 中的代碼會輸出一個 sun.jdbc.odbc.JdbcOdbcDriver
類型的對象。而仔細瀏覽 JDK 6 的目錄,這個類型正是在 %JAVA_HOME%/jre/lib/resources.jar
的 META-INF/services 目錄下的 java.sql.Driver
檔案中描述的。也就是說,這是 JDK 中預設的驅動。而如果開發人員想使得自己的驅動也能夠被 DriverManager 找到,只需要將對應的 jar 檔案加入到 CLASSPATH 中就可以了。當然,對於那些 JDBC 4.0 之前的驅動檔案,我們還是只能顯式地去載入了。
清單 8. 羅列本地機器上的 JDBC 驅動
Enumeration<Driver> drivers = DriverManager.getDrivers();while(drivers.hasMoreElements()) { System.out.println(drivers.nextElement());} |
RowId
熟悉 DB2、Oracle 等大型 DBMS 的人一定不會對 ROWID 這個概念陌生:它是資料表中一個“隱藏”的列,是每一行獨一無二的標識,表明這一行的物理或者邏輯位置。由於 ROWID 類型的廣泛使用,Java SE 6 中新增了 java.sql.RowId
的資料類型,允許 JDBC 程式能夠訪問 SQL 中的 ROWID 類型。誠然,不是所有的 DBMS 都支援 ROWID 類型。即使支援,不同的 ROWID 也會有不同的生命週期。因此使用 DatabaseMetaData.getRowIdLifetime
來判斷類型的生命週期不失為一項良好的實踐經驗。我們在 清單 1 的程式獲得串連之後增加以下代碼,便可以瞭解 ROWID 類型的支援情況。
清單 9. 瞭解 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 對象,樣本的代碼如 清單 10 所示。
清單 10. 獲得/設定 RowId 對象
// Initialize a PreparedStatementPreparedStatement pstmt = connection.prepareStatement( "SELECT rowid, name, score FROM hellotable WHERE rowid = ?");// Bind rowid into prepared statement. pstmt.setRowId(1, rowid);// Execute the statementResultSet rset = pstmt.executeQuery(); // List the recordswhile(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 對象。當獲得這個對象之後,便可以利用 setString
、setBinaryStream
、setCharacterStream
或者 setResult
等方法來初始化所表示的 XML 資料。以 setCharacterStream
為例,清單 11 表示了一個 SQLXML 對象如何擷取 java.io.Writer
對象,從外部的 XML 檔案中逐行讀取內容,從而完成初始化。
清單 11. 利用 setCharacterStream 方法來初始化 SQLXML 對象
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.Array
、Clob
中都有出現。
至於如何使用 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>
介面。清單 12 實現了 清單 1 程式的異常處理機制。這樣簡潔地遍曆了每一個 SQLException 和它潛在的原因(cause)。
清單 12. SQLException 的 for-each loop
// Java 6 codecatch (Throwable e) { if (e instanceof SQLException) { for(Throwable ex : (SQLException)e ){ System.err.println(ex.toString()); } }} |
此外,圖 4 表示了全部的 SQLException 異常體系。除去原有的 SQLException 的子類,Java 6 中新增的異常類被分為 3 種:SQLReoverableException
、SQLNonTransientException
、SQLTransientException
。在 SQLNonTransientException
和 SQLTransientException
之下還有若干子類,詳細地區分了 JDBC 程式中可能出現的各種錯誤情況。大多數子類都會有對應的標準 SQLState
值,很好地將 SQL 標準和 Java 6 類庫結合在一起。
圖 4. SQLException 異常體系
在眾多的異常類中,比較常見的有 SQLFeatureNotSupportedException
,用來表示 JDBC 驅動不支援某項 JDBC 的特性。例如在 Derby 下運行 清單 10 中的程式,就可以發現 Derby 的驅動並不支援 RowId 的特性。另外值得一提的是,SQLClientInfoException
直接繼承自 SQLException,表示當一些用戶端的屬性不能被設定在一個資料庫連接時所發生的異常。
小結:更多新特性與展望
在本文中,我們已經向讀者介紹了 Java SE 6 中 JDBC 最重要的一些新特性:它們包括嵌在 JDK 中的 Java DB (Derby)和 JDBC 4.0 的一部分。當然,還有很多本文還沒有覆蓋到的新特性。比如增加了對 SQL 語言中 NCHAR
、NVARCHAR
、LONGNVARCHAR
和 NCLOB
類型的支援;在資料庫連接池的環境下為管理 Statement
對象提供更多靈活、便利的方法等。
此外,在 Java SE 6 的 beta 版中,曾經將 Annotation Query 的特性包含進來。這項特性定義了一系列 Query 和 DataSet 介面,程式員可以通過撰寫一些 Annotation 來自訂查詢並獲得定製的資料集結果。但是,由於這一特性的參考實現最終不能滿足 JDK 的品質需求,Sun 公司忍痛割愛,取消了在 Java SE 6 中發布其的計劃。我們有理由相信,在以後的 JDK 版本中,這一特性以及更多新的功能將被包含進來,利用 Java 語言構建資料庫的應用也會變得更為自然、順暢。
參考資料
- 閱讀 Java SE 6 新特性系列 文章的完整列表,瞭解 Java SE 6 其它重要的增強。
- Java SE 6 文檔:Java SE 6 的規範文檔,可以找到絕大部分新特性的官方說明。
- 參考 Sun 公司 Java SE 6 關於 JDBC 的 API 參考文檔:java.sql 和 javax.sql。
- developerWorks Apache Derby 項目資源中心:更多關於 Apache Derby 項目的技術文章和教程 。
- Java DB at a Glance:關於 Java DB 的介紹。
- Apache Derby: Quick Start:Apache Derby 的快速入門手冊。
- 參考 JDBC 4.0 API 規範。
關於作者
|
|
|
曹若沈,上海交通大學電腦系的研究生。她對 Java SE 類庫開發和 RIA 編程都有興趣,並參加過一些相關的項目。您可以通過 ruoshen_c@sjtu.edu.cn 聯絡到她。 |