PostgreSQL 與 Java 實戰系列:五、JDBC 程式撰寫

本系列是之前應 Newzilla 邀稿而發表刊載的文章,現在在本站重新整理後發表。本篇介紹 JDBC 概念、JDBC 基礎與連線、相關資料庫類別介紹,並在最後實做 Java 程式並利用 JDBC 去存取 PostgreSQL 資料庫。

  1. 資料庫系統簡介
  2. PostgreSQL 簡介
  3. 資料庫應用實作
  4. SQL 簡介
  5. JDBC 程式撰寫(本篇)

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 的 subprotocalpostgresql,而 resource 就是資料庫名稱。另外在 PostgreSQL 之中, hostport 可以省略,所以上述的 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 型態
BOOLEANboolean
SMALLINTshort
INTEGERint
BIGINTlong
NUMERICDECIMALjava.math.BigDecimal
REALfloat
DOUBLEdouble
CHARVARCHARTEXTString
DATEjava.sql.Date
TIMEjava.sql.Time
TIMESTAMPjava.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) {
    }
  }
}

本文從資料庫一路介紹到此,相信大家都已有了基本的概念。若要想要更深入的了解,可參考下列網址:

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

sixteen + fifteen =

返回頂端