Java SE 6 新特性系列 第 5 部分,Java DB 和 JDBC 4.0

長久以來,由於大量(甚至幾乎所有)的 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 程序的執行結果
圖 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. 內嵌模式的侷限
圖 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 網絡服務器模式架構
圖 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_UNSUPPORTEDROWID_VALID_FOREVERROWID_VALID_OTHERROWID_VALID_SESSIONROWID_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 對象。當獲得這個對象之後,便可以利用 setStringsetBinaryStreamsetCharacterStream 或者 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.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> 接口。清單 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());
        }
    }
} 

此外,圖 4 表示了全部的 SQLException 異常體系。除去原有的 SQLException 的子類,Java 6 中新增的異常類被分爲 3 種:SQLReoverableExceptionSQLNonTransientExceptionSQLTransientException。在 SQLNonTransientExceptionSQLTransientException 之下還有若干子類,詳細地區分了 JDBC 程序中可能出現的各種錯誤情況。大多數子類都會有對應的標準 SQLState 值,很好地將 SQL 標準和 Java 6 類庫結合在一起。


圖 4. SQLException 異常體系
圖 4. SQLException 異常體系

在衆多的異常類中,比較常見的有 SQLFeatureNotSupportedException,用來表示 JDBC 驅動不支持某項 JDBC 的特性。例如在 Derby 下運行 清單 10 中的程序,就可以發現 Derby 的驅動並不支持 RowId 的特性。另外值得一提的是,SQLClientInfoException 直接繼承自 SQLException,表示當一些客戶端的屬性不能被設置在一個數據庫連接時所發生的異常。





回頁首


小結:更多新特性與展望

在本文中,我們已經向讀者介紹了 Java SE 6 中 JDBC 最重要的一些新特性:它們包括嵌在 JDK 中的 Java DB (Derby)和 JDBC 4.0 的一部分。當然,還有很多本文還沒有覆蓋到的新特性。比如增加了對 SQL 語言中 NCHARNVARCHARLONGNVARCHARNCLOB 類型的支持;在數據庫連接池的環境下爲管理 Statement 對象提供更多靈活、便利的方法等。

此外,在 Java SE 6 的 beta 版中,曾經將 Annotation Query 的特性包含進來。這項特性定義了一系列 Query 和 DataSet 接口,程序員可以通過撰寫一些 Annotation 來自定義查詢並獲得定製的數據集結果。但是,由於這一特性的參考實現最終不能滿足 JDK 的質量需求,Sun 公司忍痛割愛,取消了在 Java SE 6 中發佈其的計劃。我們有理由相信,在以後的 JDK 版本中,這一特性以及更多新的功能將被包含進來,利用 Java 語言構建數據庫的應用也會變得更爲自然、順暢。

 

 

摘自:http://www.ibm.com/developerworks/cn/java/j-lo-jse65/index.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章