Java SE 6 新特性: Java DB 和 JDBC 4.0

xuehongliang發表於2007-09-04
來源:developerWorks 中國

  長久以來,由於大量(甚至幾乎所有)的 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 score");
while(rs.next()) {
StringBuilder builder = new StringBuilder(rs.getString(1));
builder.append(" ");
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%dblibderby.jar 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 程式。結果一個的結果與剛才一致,而另一個卻出現了錯誤。

  錯誤的原因其實很簡單:在使用內嵌模式時,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 埠。

清單 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 FilesJavajdk1.6.0dblibderby.jar";
"C:Program FilesJavajdk1.6.0dblibderbynet.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 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 物件。當獲得這個物件之後,便可以利用 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 code
catch (Throwable e) {
if (e instanceof SQLException) {
for(Throwable ex : (SQLException)e ){
System.err.println(ex.toString());
}
}
}

  除去原有的 SQLException 的子類,Java 6 中新增的異常類被分為 3 種:SQLReoverableException、SQLNonTransientException、SQLTransientException。在 SQLNonTransientException 和 SQLTransientException 之下還有若干子類,詳細地區分了 JDBC 程式中可能出現的各種錯誤情況。大多數子類都會有對應的標準 SQLState 值,很好地將 SQL 標準和 Java 6 類庫結合在一起。

  在眾多的異常類中,比較常見的有 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 語言構建資料庫的應用也會變得更為自然、順暢。[@more@]

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/800861/viewspace-967832/,如需轉載,請註明出處,否則將追究法律責任。