# 15.7 用JDBC連接數據庫
據估算,將近一半的軟件開發都要涉及客戶(機)/服務器方面的操作。Java為自己保證的一項出色能力就是構建與平臺無關的客戶端/服務器數據庫應用。在Java 1.1中,這一保證通過Java數據庫連接(JDBC)實現了。
數據庫最主要的一個問題就是各家公司之間的規格大戰。確實存在一種“標準”數據庫語言,即“結構查詢語言”(SQL-92),但通常都必須確切知道自己要和哪家數據庫公司打交道,否則極易出問題,盡管存在所謂的“標準”。JDBC是面向“與平臺無關”設計的,所以在編程的時候不必關心自己要使用的是什么數據庫產品。然而,從JDBC里仍有可能發出對某些數據庫公司專用功能的調用,所以仍然不可任性妄為。
和Java中的許多API一樣,JDBC也做到了盡量的簡化。我們發出的方法調用對應于從數據庫收集數據時想當然的做法:同數據庫連接,創建一個語句并執行查詢,然后處理結果集。
為實現這一“與平臺無關”的特點,JDBC為我們提供了一個“驅動程序管理器”,它能動態維護數據庫查詢所需的所有驅動程序對象。所以假如要連接由三家公司開發的不同種類的數據庫,就需要三個單獨的驅動程序對象。驅動程序對象會在裝載時由“驅動程序管理器”自動注冊,并可用`Class.forName()`強行裝載。
為打開一個數據庫,必須創建一個“數據庫URL”,它要指定下述三方面的內容:
(1) 用`jdbc`指出要使用JDBC。
(2) “子協議”:驅動程序的名字或者一種數據庫連接機制的名稱。由于JDBC的設計從ODBC吸收了許多靈感,所以可以選用的第一種子協議就是“jdbc-odbc橋”,它用`odbc`關鍵字即可指定。
(3) 數據庫標識符:隨使用的數據庫驅動程序的不同而變化,但一般都提供了一個比較符合邏輯的名稱,由數據庫管理軟件映射(對應)到保存了數據表的一個物理目錄。為使自己的數據庫標識符具有任何含義,必須用自己的數據庫管理軟件為自己喜歡的名字注冊(注冊的具體過程又隨運行平臺的不同而變化)。
所有這些信息都統一編譯到一個字符串里,即“數據庫URL”。舉個例子來說,若想通過ODBC子協議同一個標識為`people`的數據庫連接,相應的數據庫URL可設為:
```
String dbUrl = "jdbc:odbc:people"
```
如果通過一個網絡連接,數據庫URL也需要包含對遠程機器進行標識的信息。
準備好同數據庫連接后,可調用靜態方法`DriverManager.getConnection()`,將數據庫的URL以及進入那個數據庫所需的用戶名密碼傳遞給它。得到的返回結果是一個`Connection`對象,利用它即可查詢和操縱數據庫。
下面這個例子將打開一個聯絡信息數據庫,并根據命令行提供的參數查詢一個人的姓(Last Name)。它只選擇那些有E-mail地址的人的名字,然后列印出符合查詢條件的所有人:
```
//: Lookup.java
// Looks up email addresses in a
// local database using JDBC
import java.sql.*;
public class Lookup {
public static void main(String[] args) {
String dbUrl = "jdbc:odbc:people";
String user = "";
String password = "";
try {
// Load the driver (registers itself)
Class.forName(
"sun.jdbc.odbc.JdbcOdbcDriver");
Connection c = DriverManager.getConnection(
dbUrl, user, password);
Statement s = c.createStatement();
// SQL code:
ResultSet r =
s.executeQuery(
"SELECT FIRST, LAST, EMAIL " +
"FROM people.csv people " +
"WHERE " +
"(LAST='" + args[0] + "') " +
" AND (EMAIL Is Not Null) " +
"ORDER BY FIRST");
while(r.next()) {
// Capitalization doesn't matter:
System.out.println(
r.getString("Last") + ", "
+ r.getString("fIRST")
+ ": " + r.getString("EMAIL") );
}
s.close(); // Also closes ResultSet
} catch(Exception e) {
e.printStackTrace();
}
}
} ///:~
```
可以看到,數據庫URL的創建過程與我們前面講述的完全一樣。在該例中,數據庫未設密碼保護,所以用戶名和密碼都是空串。
用`DriverManager.getConnection()`建好連接后,接下來可根據結果`Connection`對象創建一個`Statement`(語句)對象,這是用`createStatement()`方法實現的。根據結果`Statement`,我們可調用`executeQuery()`,向其傳遞包含了SQL-92標準SQL語句的一個字符串(不久就會看到如何自動創建這類語句,所以沒必要在這里知道關于SQL更多的東西)。
`executeQuery()`方法會返回一個`ResultSet`(結果集)對象,它與迭代器非常相似:`next()`方法將迭代器移至語句中的下一條記錄;如果已抵達結果集的末尾,則返回`null`。我們肯定能從`executeQuery()`返回一個`ResultSet`對象,即使查詢結果是個空集(也就是說,不會產生一個異常)。注意在試圖讀取任何記錄數據之前,都必須調用一次`next()`。若結果集為空,那么對`next()`的這個首次調用就會返回`false`。對于結果集中的每條記錄,都可將字段名作為字符串使用(當然還有其他方法),從而選擇不同的字段。另外要注意的是字段名的大小寫是無關緊要的——SQL數據庫不在乎這個問題。為決定返回的類型,可調用`getString()`,`getFloat()`等等。到這個時候,我們已經用Java的原始格式得到了自己的數據庫數據,接下去可用Java代碼做自己想做的任何事情了。
## 15.7.1 讓示例運行起來
就JDBC來說,代碼本身是很容易理解的。最令人迷惑的部分是如何使它在自己特定的系統上運行起來。之所以會感到迷惑,是由于它要求我們掌握如何才能使JDBC驅動程序正確裝載,以及如何用我們的數據庫管理軟件來設置一個數據庫。
當然,具體的操作過程在不同的機器上也會有所區別。但這兒提供的在32位Windows環境下操作過程可有效幫助大家理解在其他平臺上的操作。
(1) 步驟1:尋找JDBC驅動程序
上述程序包含了下面這條語句:
```
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
```
這似乎暗示著一個目錄結構,但大家不要被它蒙騙了。在我手上這個JDK 1.1安裝版本中,根本不存在叫作`JdbcOdbcDriver.class`的一個文件。所以假如在看了這個例子后去尋找它,那么必然會徒勞而返。另一些人提供的例子使用的是一個假名字,如`myDriver.ClassName`,但人們從字面上得不到任何幫助。事實上,上述用于裝載jdbc-odbc驅動程序(實際是與JDK 1.1配套提供的唯一驅動)的語句在聯機文檔的多處地方均有出現(特別是在一個標記為“JDBC-ODBC Bridge Driver”的頁內)。若上面的裝載語句不能工作,那么它的名字可能已隨著Java新版本的發布而改變了;此時應到聯機文檔里尋找新的表述方式。
若裝載語句出錯,會在這個時候得到一個異常。為了檢驗驅動程序裝載語句是不是能正常工作,請將該語句后面直到`catch`從句之間的代碼暫時設為注釋。如果程序運行時未出現異常,表明驅動程序的裝載是正確的。
(2) 步驟2:配置數據庫
同樣地,我們只限于在32位Windows環境中工作;您可能需要研究一下自己的操作系統,找出適合自己平臺的配置方法。
首先打開控制面板。其中可能有兩個圖標都含有“ODBC”字樣,必須選擇那個“32位ODBC”,因為另一個是為了保持與16位軟件的向后兼容而設置的,和JDBC混用沒有任何結果。雙擊“32位ODBC”圖標后,看到的應該是一個卡片式對話框,上面一排有多個卡片標簽,其中包括“用戶DSN”、“系統DSN”、“文件DSN”等等。其中,“DSN”代表“數據源名稱”(Data Source Name)。它們都與JDBC-ODBC橋有關,但設置數據庫時唯一重要的地方“系統DSN”。盡管如此,由于需要測試自己的配置以及創建查詢,所以也需要在“文件DSN”中設置自己的數據庫。這樣便可讓Microsoft Query工具(與Microsoft Office配套提供)正確地找到數據庫。注意一些軟件公司也設計了自己的查詢工具。
最有趣的數據庫是我們已經使用過的一個。標準ODBC支持多種文件格式,其中包括由不同公司專用的一些格式,如dBASE。然而,它也包括了簡單的“逗號分隔ASCII”格式,它幾乎是每種數據工具都能夠生成的。就目前的例子來說,我只選擇自己的`people`數據庫。這是我多年來一直在維護的一個數據庫,中間使用了各種聯絡管理工具。我把它導出成為一個逗號分隔的ASCII文件(一般有個`.csv`擴展名,用Outlook Express導出通信簿時亦可選用同樣的文件格式)。在“文件DSN”區域,我按下“添加”按鈕,選擇用于控制逗號分隔ASCII文件的文本驅動程序(Microsoft Text Driver),然后撤消對“使用當前目錄”的選擇,以便導出數據文件時可以自行指定目錄。
大家會注意到在進行這些工作的時候,并沒有實際指定一個文件,只是一個目錄。那是因為數據庫通常是由某個目錄下的一系列文件構成的(盡管也可能采用其他形式)。每個文件一般都包含了單個“數據表”,而且SQL語句可以產生從數據庫中多個表摘取出來的結果(這叫作“聯合”,或者`join`)只包含了單張表的數據庫(就象目前這個)通常叫作“平面文件數據庫”。對于大多數問題,如果已經超過了簡單的數據存儲與獲取力所能及的范圍,那么必須使用多個數據表。通過“聯合”,從而獲得希望的結果。我們把這些叫作“關系型”數據庫。
(3) 步驟3:測試配置
為了對配置進行測試,需用一種方式核實數據庫是否可由查詢它的一個程序“見到”。當然,可以簡單地運行上述的JDBC示范程序,并加入下述語句:
```
Connection c = DriverManager.getConnection(
dbUrl, user, password);
```
若拋出一個異常,表明你的配置有誤。
然而,此時很有必要使用一個自動化的查詢生成工具。我使用的是與Microsoft Office配套提供的Microsoft Query,但你完全可以自行選擇一個。查詢工具必須知道數據庫在什么地方,而Microsoft Query要求我進入ODBC Administrator的“文件DSN”卡片,并在那里新添一個條目。同樣指定文本驅動程序以及保存數據庫的目錄。雖然可將這個條目命名為自己喜歡的任何東西,但最好還是使用與“系統DSN”中相同的名字。
做完這些工作后,再用查詢工具創建一個新查詢時,便會發現自己的數據庫可以使用了。
(4) 步驟4:建立自己的SQL查詢
我用Microsoft Query創建的查詢不僅指出目標數據庫存在且次序良好,也會自動生成SQL代碼,以便將其插入我自己的Java程序。我希望這個查詢能夠檢查記錄中是否存在與啟動Java程序時在命令行鍵入的相同的“姓”(Last Name)。所以作為一個起點,我搜索自己的姓`Eckel`。另外,我希望只顯示出有對應E-mail地址的那些名字。創建這個查詢的步驟如下:
(1) 啟動一個新查詢,并使用查詢向導(Query Wizard)。選擇`people`數據庫(等價于用適應的數據庫URL打開數據庫連接)。
(2) 選擇數據庫中的`people`表。從這張數據表中,選擇`FIRST`,`LAST`和`EMAIL`列。
(3) 在“Filter Data”(過濾器數據庫)下,選擇`LAST`,并選擇`equals`(等于),加上參數`Eckel`。點選“And”單選鈕。
(4) 選擇`EMAIL`,并選中“Is not Null”(不為空)。
(5) 在“Sort By”下,選擇`FIRST`。
查詢結果會向我們展示出是否能得到自己希望的東西。
現在可以按下SQL按鈕。不需要我們任何方面的介入,正確的SQL代碼會立即彈現出來,以便我們粘貼和復制。對于這個查詢,相應的SQL代碼如下:
```
SELECT people.FIRST, people.LAST, people.EMAIL
FROM people.csv people
WHERE (people.LAST='Eckel') AND
(people.EMAIL Is Not Null)
ORDER BY people.FIRST
```
若查詢比較復雜,手工編碼極易出錯。但利用一個查詢工具,就可以交互式地測試自己的查詢,并自動獲得正確的代碼。事實上,親手為這些事情編碼是難以讓人接受的。
(5) 步驟5:在自己的查詢中修改和粘貼
我們注意到上述代碼與程序中使用的代碼是有所區別的。那是由于查詢工具對所有名字都進行了限定,即便涉及的僅有一個數據表(若真的涉及多個數據表,這種限定可避免來自不同表的同名數據列發生沖突)。由于這個查詢只需要用到一個數據表,所以可考慮從大多數名字中刪除“people”限定符,就象下面這樣:
```
SELECT FIRST, LAST, EMAIL
FROM people.csv people
WHERE (LAST='Eckel') AND
(EMAIL Is Not Null)
ORDER BY FIRST
```
此外,我們不希望“硬編碼”這個程序,從而只能查找一個特定的名字。相反,它應該能查找我們在命令行動態提供的一個名字。所以還要進行必要的修改,并將SQL語句轉換成一個動態生成的字符串。如下所示:
```
"SELECT FIRST, LAST, EMAIL " +
"FROM people.csv people " +
"WHERE " +
"(LAST='" + args[0] + "') " +
" AND (EMAIL Is Not Null) " +
"ORDER BY FIRST");
```
SQL還有一種方式可將名字插入一個查詢,名為“過程”(`Procedures`),它的速度非常快。但對于我們的大多數實驗性數據庫操作,以及一些初級應用,用Java構建查詢字符串已經很不錯了。
從這個例子可以看出,利用目前找得到的工具——特別是查詢構建工具——涉及SQL及JDBC的數據庫編程是非常簡單和直觀的。
## 15.7.2 查找程序的GUI版本
最好的方法是讓查找程序一直保持運行,要查找什么東西時只需簡單地切換到它,并鍵入要查找的名字即可。下面這個程序將查找程序作為一個“application/applet”創建,且添加了名字自動填寫功能,所以不必鍵入完整的姓,即可看到數據:
```
//: VLookup.java
// GUI version of Lookup.java
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import java.sql.*;
public class VLookup extends Applet {
String dbUrl = "jdbc:odbc:people";
String user = "";
String password = "";
Statement s;
TextField searchFor = new TextField(20);
Label completion =
new Label(" ");
TextArea results = new TextArea(40, 20);
public void init() {
searchFor.addTextListener(new SearchForL());
Panel p = new Panel();
p.add(new Label("Last name to search for:"));
p.add(searchFor);
p.add(completion);
setLayout(new BorderLayout());
add(p, BorderLayout.NORTH);
add(results, BorderLayout.CENTER);
try {
// Load the driver (registers itself)
Class.forName(
"sun.jdbc.odbc.JdbcOdbcDriver");
Connection c = DriverManager.getConnection(
dbUrl, user, password);
s = c.createStatement();
} catch(Exception e) {
results.setText(e.getMessage());
}
}
class SearchForL implements TextListener {
public void textValueChanged(TextEvent te) {
ResultSet r;
if(searchFor.getText().length() == 0) {
completion.setText("");
results.setText("");
return;
}
try {
// Name completion:
r = s.executeQuery(
"SELECT LAST FROM people.csv people " +
"WHERE (LAST Like '" +
searchFor.getText() +
"%') ORDER BY LAST");
if(r.next())
completion.setText(
r.getString("last"));
r = s.executeQuery(
"SELECT FIRST, LAST, EMAIL " +
"FROM people.csv people " +
"WHERE (LAST='" +
completion.getText() +
"') AND (EMAIL Is Not Null) " +
"ORDER BY FIRST");
} catch(Exception e) {
results.setText(
searchFor.getText() + "\n");
results.append(e.getMessage());
return;
}
results.setText("");
try {
while(r.next()) {
results.append(
r.getString("Last") + ", "
+ r.getString("fIRST") +
": " + r.getString("EMAIL") + "\n");
}
} catch(Exception e) {
results.setText(e.getMessage());
}
}
}
public static void main(String[] args) {
VLookup applet = new VLookup();
Frame aFrame = new Frame("Email lookup");
aFrame.addWindowListener(
new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
aFrame.add(applet, BorderLayout.CENTER);
aFrame.setSize(500,200);
applet.init();
applet.start();
aFrame.setVisible(true);
}
} ///:~
```
數據庫的許多邏輯都是相同的,但大家可看到這里添加了一個`TextListener`,用于監視在`TextField`(文本字段)的輸入。所以只要鍵入一個新字符,它首先就會試著查找數據庫中的“姓”,并顯示出與當前輸入相符的第一條記錄(將其置入`completion Label`,并用它作為要查找的文本)。因此,只要我們鍵入了足夠的字符,使程序能找到與之相符的唯一一條記錄,就可以停手了。
## 15.7.3 JDBC API為何如何復雜
閱覽JDBC的聯機幫助文檔時,我們往往會產生畏難情緒。特別是`DatabaseMetaData`接口——與Java中看到的大多數接口相反,它的體積顯得非常龐大——存在著數量眾多的方法,比如`dataDefinitionCausesTransactionCommit()`,`getMaxColumnNameLength()`,`getMaxStatementLength()`,`storesMixedCaseQuotedIdentifiers()`,`supportsANSI92IntermediateSQL()`,`supportsLimitedOuterJoins()`等等。它們有這兒有什么意義嗎?
正如早先指出的那樣,數據庫起初一直處于一種混亂狀態。這主要是由于各種數據庫應用提出的要求造成的,所以數據庫工具顯得非常“強大”——換言之,“龐大”。只是近幾年才涌現出了SQL的通用語言(常用的還有其他許多數據庫語言)。但即便象SQL這樣的“標準”,也存在無數的變種,所以JDBC必須提供一個巨大的`DatabaseMetaData`接口,使我們的代碼能真正利用當前要連接的一種“標準”SQL數據庫的能力。簡言之,我們可編寫出簡單的、能移植的SQL。但如果想優化代碼的執行速度,那么為了適應不同數據庫類型的特點,我們的編寫代碼的麻煩就大了。
當然,這并不是Java的缺陷。數據庫產品之間的差異是我們和JDBC都要面對的一個現實。但是,如果能編寫通用的查詢,而不必太關心性能,那么事情就要簡單得多。即使必須對性能作一番調整,只要知道最終面向的平臺,也不必針對每一種情況都編寫不同的優化代碼。
在Sun發布的Java 1.1產品中,配套提供了一系列電子文檔,其中有對JDBC更全面的介紹。此外,在由Hamilton Cattel和Fisher編著、Addison-Wesley于1997年出版的《JDBC Database Access with Java》中,也提供了有關這一主題的許多有用資料。同時,書店里也經常出現一些有關JDBC的新書。
- Java 編程思想
- 寫在前面的話
- 引言
- 第1章 對象入門
- 1.1 抽象的進步
- 1.2 對象的接口
- 1.3 實現方案的隱藏
- 1.4 方案的重復使用
- 1.5 繼承:重新使用接口
- 1.6 多態對象的互換使用
- 1.7 對象的創建和存在時間
- 1.8 異常控制:解決錯誤
- 1.9 多線程
- 1.10 永久性
- 1.11 Java和因特網
- 1.12 分析和設計
- 1.13 Java還是C++
- 第2章 一切都是對象
- 2.1 用引用操縱對象
- 2.2 所有對象都必須創建
- 2.3 絕對不要清除對象
- 2.4 新建數據類型:類
- 2.5 方法、參數和返回值
- 2.6 構建Java程序
- 2.7 我們的第一個Java程序
- 2.8 注釋和嵌入文檔
- 2.9 編碼樣式
- 2.10 總結
- 2.11 練習
- 第3章 控制程序流程
- 3.1 使用Java運算符
- 3.2 執行控制
- 3.3 總結
- 3.4 練習
- 第4章 初始化和清除
- 4.1 用構造器自動初始化
- 4.2 方法重載
- 4.3 清除:收尾和垃圾收集
- 4.4 成員初始化
- 4.5 數組初始化
- 4.6 總結
- 4.7 練習
- 第5章 隱藏實現過程
- 5.1 包:庫單元
- 5.2 Java訪問指示符
- 5.3 接口與實現
- 5.4 類訪問
- 5.5 總結
- 5.6 練習
- 第6章 類復用
- 6.1 組合的語法
- 6.2 繼承的語法
- 6.3 組合與繼承的結合
- 6.4 到底選擇組合還是繼承
- 6.5 protected
- 6.6 累積開發
- 6.7 向上轉換
- 6.8 final關鍵字
- 6.9 初始化和類裝載
- 6.10 總結
- 6.11 練習
- 第7章 多態性
- 7.1 向上轉換
- 7.2 深入理解
- 7.3 覆蓋與重載
- 7.4 抽象類和方法
- 7.5 接口
- 7.6 內部類
- 7.7 構造器和多態性
- 7.8 通過繼承進行設計
- 7.9 總結
- 7.10 練習
- 第8章 對象的容納
- 8.1 數組
- 8.2 集合
- 8.3 枚舉器(迭代器)
- 8.4 集合的類型
- 8.5 排序
- 8.6 通用集合庫
- 8.7 新集合
- 8.8 總結
- 8.9 練習
- 第9章 異常差錯控制
- 9.1 基本異常
- 9.2 異常的捕獲
- 9.3 標準Java異常
- 9.4 創建自己的異常
- 9.5 異常的限制
- 9.6 用finally清除
- 9.7 構造器
- 9.8 異常匹配
- 9.9 總結
- 9.10 練習
- 第10章 Java IO系統
- 10.1 輸入和輸出
- 10.2 增添屬性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File類
- 10.5 IO流的典型應用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 壓縮
- 10.9 對象序列化
- 10.10 總結
- 10.11 練習
- 第11章 運行期類型識別
- 11.1 對RTTI的需要
- 11.2 RTTI語法
- 11.3 反射:運行期類信息
- 11.4 總結
- 11.5 練習
- 第12章 傳遞和返回對象
- 12.1 傳遞引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只讀類
- 12.5 總結
- 12.6 練習
- 第13章 創建窗口和程序片
- 13.1 為何要用AWT?
- 13.2 基本程序片
- 13.3 制作按鈕
- 13.4 捕獲事件
- 13.5 文本字段
- 13.6 文本區域
- 13.7 標簽
- 13.8 復選框
- 13.9 單選鈕
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 視窗化應用
- 13.16 新型AWT
- 13.17 Java 1.1用戶接口API
- 13.18 可視編程和Beans
- 13.19 Swing入門
- 13.20 總結
- 13.21 練習
- 第14章 多線程
- 14.1 反應靈敏的用戶界面
- 14.2 共享有限的資源
- 14.3 堵塞
- 14.4 優先級
- 14.5 回顧runnable
- 14.6 總結
- 14.7 練習
- 第15章 網絡編程
- 15.1 機器的標識
- 15.2 套接字
- 15.3 服務多個客戶
- 15.4 數據報
- 15.5 一個Web應用
- 15.6 Java與CGI的溝通
- 15.7 用JDBC連接數據庫
- 15.8 遠程方法
- 15.9 總結
- 15.10 練習
- 第16章 設計模式
- 16.1 模式的概念
- 16.2 觀察器模式
- 16.3 模擬垃圾回收站
- 16.4 改進設計
- 16.5 抽象的應用
- 16.6 多重分發
- 16.7 訪問器模式
- 16.8 RTTI真的有害嗎
- 16.9 總結
- 16.10 練習
- 第17章 項目
- 17.1 文字處理
- 17.2 方法查找工具
- 17.3 復雜性理論
- 17.4 總結
- 17.5 練習
- 附錄A 使用非JAVA代碼
- 附錄B 對比C++和Java
- 附錄C Java編程規則
- 附錄D 性能
- 附錄E 關于垃圾收集的一些話
- 附錄F 推薦讀物