長久以來,因爲大量(甚至幾乎全部)的 Java 應用都依賴於數據庫,如何使用 Java 語言高效、可靠、簡潔地訪問數據庫一直是程序員們津津樂道的話題。新發布的 Java SE 6 也在這方面更上層樓,爲編程人員提供了許多好用的新特性。其中最顯著的,莫過於 Java SE 6 擁有了一個內嵌的 100% 用 Java 語言編寫的數據庫系統。而且,Java 6 開始支持 JDBC 4.0 的一系列新功能和屬性。這樣,Java SE 在對持久數據的訪問上就顯得更爲易用和強大了。sql
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;向表內插入了兩條數據;而後,查詢數據並將結果打印在控制檯上;最後,刪除表和數據庫,釋放資源。apache
清單 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 是執行結果的一部分截屏:api
圖 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
熟悉 DB二、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()); } } }
此外,圖 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 規範。