本系列是之前應 Newzilla 邀稿而發表刊載的文章,現在在本站重新整理後發表。本篇介紹 JDBC 概念、JDBC 基礎與連線、相關資料庫類別介紹,並在最後實做 Java 程式並利用 JDBC 去存取 PostgreSQL 資料庫。
目錄
JDBC 概念
早期我們要寫程式去存取資料庫是件很辛苦的事,因為每個資料庫都擁有各自的存取介面和不同語言的 API,Java 語言出現後,也同樣地面臨到資料庫的問題。如果依照早期的做法,那就必須要等待各個資料庫廠商或熱心的程式設計人員撰寫出可讓 Java 使用、各式各樣且架構不同的 API,或者冒著「需放棄跨平台優點」的風險來改用 JNI 去呼叫外部原生碼。如果今天我們需要將資料從 A 資料庫轉移至 B 資料庫的話,那麼資料庫存取模組的程式便要改寫,甚至可能範圍大到整個程式的架構都要重新設計。上述的方法缺乏易用性、維護性和延展性,對整個程式架構和公司組織來說並不是件好事。
因此,為了解決這個問題,讓 Java 和各個資料庫之間都可以順利地搭起一座橋樑,我們就有了 JDBC(Java Database Connectivity)介面。JDBC API 提供一系列的類別和方法,讓我們可以執行資料庫連線、查詢、修改、新增和刪除資料、檢視欄位資訊等工作,它也是 Java 程式設計人員和資料庫開發者之間可以共同依循的標準。所以今天不管是那一種資料庫(甚至是文字檔形式的資料庫),只要它有提供其所屬的 JDBC API,那麼我們就可以撰寫 Java 程式通過 JDBC 去存取。
JDBC 位在 java.sql
套件之中,多數屬於介面(interface
),而實作部份就交由各個資料庫的設計人員去處理。底下針對常見的介面和類別做簡單的介紹:
DriverManager
類別
功能為載入資料庫的驅動程式,以及建立程式和資料庫之間的連線Driver
介面
資料庫驅動程式。與各家資料庫溝通的重要接口Connection
介面
資料庫的連結介面Statement
介面
執行查詢、修改、刪除 SQL 語法,並回傳結果ResultSet
介面
執行完 SQL 語法後的結果,可藉由此介面去讀取記錄DatabaseMetaData
介面
處理有關資料庫、記錄和驅動程式的資訊
下圖即為 JDBC 架構,可以大致看出各個介面之間如何合作以達到存取資料庫的目的:
Windows 平台會自動安裝 JDBC API,你可以在 PosrgreSQL 8.0\jdbc
目錄下找到四個副檔名為 jar 的檔案,pg7.4 代表檔案是提供給 PostgreSQL 7.4 所使用的,215(或 214)是它的建立編號。四個檔案的區分如下:
- jdbc1:JDK 1.1 版
- jdbc2:JDK 1.2 ,JDK 1.3 版
- jdbc2ee:包含
javax.sql
套件支援,只有 JDK 1.3 - jdbc3:JDK 1.4 版,包含 SSL 連線支援
Linux 平台需要到 https://jdbc.postgresql.org/download/ 頁面下載,你可以下載 PostgreSQL 7.4 的 JDBC 穩定版本,或是 8.0 的發展版本。同樣地,Windows 平台也可以去下載較新的 8.0 版。
取得 jar 後,可以將它複製到 Java 安裝路徑下的 jre/lib/ext
目錄下,如此一來每次執行 Java 程式時,JVM 會自動去取得其中的類別資料,而無需我們手動設定 classpath
參數。
java.sql
套件介紹
DriverManager
類別
DriverManager
負責讀入各資料庫的驅動程式,然後建立與資料庫之間的連結。最簡單的方法是使用 Class
類別中的靜態函式 forName()
載入驅動程式,然後再經由 DriverManager
取得連線:
Class.forName("org.postgresql.Driver"); // PostgreSQL 的 Driver
Connection conn = DriverManager.getConnection(url);
如果找不到驅動程式,forName
函式會丟出 ClassNotFoundException
例外,所以實際撰寫時我們必須將程式碼包在 try-catch
區段之中。上述中的 url
是一 String
物件,內容為 JDBC URL,也就是說,若要存取資料庫,那麼我們必須使用 URL 去指定。它的格式為:
jdbc:<subprotocol>://<host>:<port>/<resource>
PostgreSQL 的 subprotocal
是 postgresql
,而 resource
就是資料庫名稱。另外在 PostgreSQL 之中, host
和 port
可以省略,所以上述的 url
可以定義如下:
jdbc:<subprotocol>:<resource>
// 宣告
String url = "jdbc:postgresql:guestbook";
DriverManager
類別常用函式:
/**
* 功能:取得連線物件
* 參數:url - JDBC URL
*/
public static Connection getConnection(String url)
throws SQLException {}
/**
* 功能:取得連線物件
* 參數:url - JDBC URL
* user - 資料庫的使用者帳號
* password - 資料庫的使用者密碼
*/
public static Connection getConnection(String url,
String user, String password) throws SQLException {}
Connection
介面
此介面處理與資料庫之間的連線,可以使用它建立 Statement 物件以執行 SQL 語法:
Statement stat = conn.createStatement();
Connection
介面常用函式:
/**
* 功能:關閉資料庫連線並釋放資源
*/
public void close() throws SQLException {}
/**
* 功能:建立 SQL 敘述物件
*/
public Statement createStatement() throws SQLException {}
/**
* 功能:取得資料庫資訊
*/
public DatabaseMetaData getMetaData() throws SQLException {}
/**
* 功能:功能:檢查資料庫連線是否已中斷
* 回傳:true - 已中斷連線
*/
public boolean isClosed() throws SQLException {}
Statement
介面
執行 SQL 語法並回傳結果:
ResultSet rs = stat.executeQuery(sql);
Statement
介面常用函式:
/**
* 功能:關閉 Statement 並釋放資源
*/
public void close() throws SQLException {}
/**
* 功能:執行 SQL 指令
* 參數:sql - 一或數項 SQL 指令
* 回傳:true - 第一項 SQL 指令的執行結果為一 ResultSet 物件
* false - 執行結果為數字,或是無執行結果
*/
public boolean execute(String sql) throws SQLException {}
/**
* 功能:執行 SELECT 語法,並傳回結果
* 參數:sql - SELECT 指令
*/
public ResultSet executeQuery(String sql) throws SQLException {}
/**
* 功能:執行變更資料的 SQL 指令
* 參數:sql - INSERT, UPDATE 或 DELETE 指令
* 回傳:執行變更的記錄總筆數
*/
public int executeUpdate(String sql) throws SQLException {}
ResultSet
介面
取得資料庫中的記錄,column 是 String
字串,表示欄位:
while (rs.next()) {
System.out.println(rs.getString(column));
}
ResultSet
使用指標來依序存取資料,一開始指標會位在所有記錄的最前端,當使用 next()
時,會檢查其後端是否有記錄。如果有的話,會將指標往後移動一筆,並且回傳 true
。
ResultSet
介面常用函式(不包括取值函式):
/**
* 功能:將指標移至所有記錄之後
*/
public void afterLast() throws SQLException {}
/**
* 功能:將指標移至所有記錄之前
*/
public void beforeFirst() throws SQLException {}
/**
* 功能:關閉 ResultSet 並釋放資源
*/
public void close() throws SQLException {}
/**
* 功能:尋找 ResultSet 中欄位名其相對的索引值
* 參數:columnName - 欄位名稱
* 回傳:索引值
*/
public int findColumn(String columnName) throws SQLException {}
/**
* 功能:將指標移至第一筆記錄
* 回傳:true - 成功
* false - ResultSet 中無任何記錄
*/
public boolean first() throws SQLException {}
/**
* 功能:傳回指標目前位在第幾筆記錄上
*/
public int getRow() throws SQLException {}
/**
* 功能:檢查指標是否位於所有記錄之後
*/
public boolean isAfterLast() throws SQLException {}
/**
* 功能:檢查指標是否位於所有記錄之前
*/
public boolean isBeforeFirst() throws SQLException {}
/**
* 功能:檢查指標是否位於第一筆記錄
*/
public boolean isFirst() throws SQLException {}
/**
* 功能:檢查指標是否位於最後一筆記錄
*/
public boolean isLast() throws SQLException {}
/**
* 功能:將指標移至最後一筆記錄
* 回傳:true - 成功
* false - ResultSet 中無任何記錄
*/
public boolean last() throws SQLException {}
/**
* 功能:將指標移至下一筆記錄
* 回傳:true - 成功
* false - 其後端已無記錄
*/
public boolean next() throws SQLException {}
/**
* 功能:將指標移至上一筆記錄
* 回傳:true - 成功
* false - 其前端已無記錄
*/
public boolean previous() throws SQLException {}
從 ResultSet
物件取得欄位中的資料需呼叫相對應資料型態的取值函式,例如欲取得 INTEGER
欄位的資料,需使用 ResultSet
中的 getInt()
函式。這些類型的函式依傳入參數的不同而有兩種呼叫方式,一種以欄位索引值來取得資料,另一種則是直接傳入欄位名稱,而通常前者的效率會較快,但你必須要很清楚回傳記錄中每個索引值分屬那些型態。底下列出 SQL 與 Java 資料型態的對應,以及 ResultSet
的取值函式:
SQL 型態 | Java 型態 |
---|---|
BOOLEAN | boolean |
SMALLINT | short |
INTEGER | int |
BIGINT | long |
NUMERIC 、DECIMAL | java.math.BigDecimal |
REAL | float |
DOUBLE | double |
CHAR 、VARCHAR 、TEXT | String |
DATE | java.sql.Date |
TIME | java.sql.Time |
TIMESTAMP | java.sql.Timestamp |
getBoolean();
getByte();
getDate();
getDouble();
getFloat();
getInt();
getLong();
getObject();
getShort();
getString();
getTime();
getTimestamp();
// 呼叫方法:
String login = rs.getString(2); // 使用欄位索引值
String password = rs.getString("password"); // 或使用欄位名稱
JDBC 連線
在介紹完常用介面和函式後,接下來要開始架構 JDBC 程式。此處我們會建立一個可以存取 PostgreSQL 的簡單例子,你可以依照這個例子去延伸出更深更廣的應用。
一個基本的資料庫連線程式,需要有底下幾個步驟:
- 載入 JDBC 驅動程式
- 建立
Connection
物件 - 建立
Statement
物件 - 利用
Statement
物件去執行 SQL 語法 - 若上述執行的是
SELECT
語法,可利用回傳的ResultSet
物件取得資料 - 關閉
ResultSet
物件 - 關閉
Statement
物件 - 關閉
Connection
物件
為了節省每次存取資料庫時的連線時間,我們將建立和關閉資料庫連線的部份獨立撰寫成一個類別,日後只要使用此一類別便可以進行資料庫存取:
// StatementHandler.java
import java.sql.*;
public class StatementHandler {
private final String JURL = "jdbc:postgresql:guestbook";
private Connection conn;
private Statement stat;
public StatementHandler() throws SQLException {
try {
Class.forName("org.postgresql.Driver"); // 載入驅動程式
} catch (ClassNotFoundException ex) { // 抓取例外
ex.printStackTrace();
throw new RuntimeException("Not found driver.");
}
conn = DriverManager.getConnection(JURL, "gbadmin", "1234");
stat = conn.createStatement(); // 建立 Connection 及 Statement
}
public ResultSet query(String sql) throws SQLException {
return stat.executeQuery(sql); // 負責 SELECT
}
public int update(String sql) throws SQLException {
return stat.executeUpdate(sql); // 負責 UPDATE,INSERT,DELETE
}
public void close() throws SQLException { // 釋放資源
stat.close();
conn.close();
}
}
實做 Java 程式存取 PostgreSQL
在將連線和釋放資源的部份獨立成一個類別之後,接下來我們就可以很輕鬆地撰寫其他的功能,例如新增、查詢、修改和刪除有關會員及留言的類別。後面的例子提供了一個簡易的架構去存取資料庫,你可以自行延伸其中不足的部份,或著將其做更一步的提煉。
首先建立的是新增會員的類別:
// AddMember.java
import java.sql.*;
public class AddMember {
private StatementHandler sh;
public AddMember() {
try {
sh = new StatementHandler(); // 先取得資料庫連線物件
} catch (SQLException ex) {
ex.printStackTrace();
throw new RuntimeException("Can not access database.");
}
}
public int add(String login, String pass, int sex,
String email, int astro, int priv) {
if (login == null || pass == null) { // 檢查帳號和密碼不得為空
throw new IllegalArgumentException("Argument is a null object.");
}
StringBuffer sqla = new StringBuffer(); // 建立 SQL 語法
sqla.append("INSERT INTO member (login, password, sexy, email, ")
.append("astro, jointime, priv) VALUES ('").append(login)
.append("','").append(pass).append("',").append(sex)
.append(",'").append(email).append("',").append(astro)
.append(",now(),").append(priv).append(")");
int result = 0;
try {
sh.update(sqla.toString()); // 執行更新
} catch (SQLException ex) {
ex.printStackTrace();
throw new RuntimeException("Can not access database.");
}
return result;
}
public static void main(String[] args) {
AddMember am = new AddMember();
// 呼叫函式 add() 新增會員,此處可做適當修改以符合實際需要
am.add("test", "1234", 0, "", 1, 0);
}
}
新增留言的類別:
// AddNote.java
import java.sql.*;
public class AddNote {
private StatementHandler sh;
public AddNote() {
try {
sh = new StatementHandler();
} catch (SQLException ex) {
ex.printStackTrace();
throw new RuntimeException("Can not access database.");
}
}
public int add(int mem, String note) {
if (note == null) {
throw new IllegalArgumentException("Argument is a null object.");
}
StringBuffer sqla = new StringBuffer();
sqla.append("INSERT INTO note (member, content, posttime) VALUES (")
.append(mem).append(",'").append(note).append("',now())");
int result = 0;
try {
sh.update(sqla.toString());
} catch (SQLException ex) {
ex.printStackTrace();
throw new RuntimeException("Can not access database.");
}
return result;
}
public static void main(String[] args) {
AddNote an = new AddNote();
an.add(1, "Test test");
}
}
查詢留言的類別:
// ViewNote.java
import java.sql.*;
public class ViewNote {
private StatementHandler sh;
public ViewNote() {
try {
sh = new StatementHandler();
} catch (SQLException ex) {
ex.printStackTrace();
throw new RuntimeException("Can not access database.");
}
}
public ResultSet query(String sub) {
StringBuffer sqla = new StringBuffer();
sqla.append("SELECT * from note"); // 建立 SQL 語法
if (sub != null && sub.length() > 0) {
sqla.append(sub);
}
ResultSet rs = null;
try {
rs = sh.query(sqla.toString()); // 取得 ResultSet
} catch (SQLException ex) {
ex.printStackTrace();
throw new RuntimeException("Can not access database.");
}
return rs;
}
public static void main(String[] args) {
ViewNote vn = new ViewNote();
ResultSet rs = vn.query(null);
try {
while (rs.next()) { // 巡訪所有記錄
System.out.println(rs.getInt("noteIndex") + ":"
+ rs.getString("content")); // 取得資料
System.out.println(" " + rs.getTimestamp("posttime"));
}
} catch (SQLException ex) {
}
}
}
本文從資料庫一路介紹到此,相信大家都已有了基本的概念。若要想要更深入的了解,可參考下列網址: