Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Latest commit

 

History

History
History
1035 lines (798 loc) · 46.7 KB

File metadata and controls

1035 lines (798 loc) · 46.7 KB
Copy raw file
Download raw file
Outline
Edit and raw actions

第 20 章 JDBC 入門

JDBC(Java DataBase Connectivity)是用於執行 SQL 的 Java 解決方案,它將不同資料庫之間各自差異 API 與標準的 SQL(Structured Query Language)陳述分開看待,實現資料庫無關的 Java 操作介面,開發人員使用 JDBC 統一的介面,並專注於標準的 SQL 陳述,就可以避免底層資料庫驅動程式與相關操作介面的差異性。

實際的資料庫存取是個非常複雜的主題,可以使用專書加以講解說明,不過在這個章節中,會告訴您一些 JDBC 基本 API 的使用與觀念,讓您對 Java 如何存取資料庫有所認識。


20.1 使用 JDBC 連接資料庫

在正式使用 JDBC 進行資料庫操作之前,先來認識一下 JDBC 的基本架構,了解資料庫驅動程式與資料庫之間的關係,在這個小節也將看到,如何設計一個簡單的工具類別,讓您在進行資料庫連接(Connection)時更為方便。

20.1.1 簡介 JDBC

如果要連接資料庫並進行操作,基本上必須了解所使用的資料庫所提供的 API 操作介面,然而各個廠商所提供的 API 操作介面並不一致,如果今天要使用A廠商的資料庫,就必須設計一個專用的程式來操作 A 廠商資料庫所提供的 API,將來如果要使用 B 廠商的資料庫,即使目的相同,也是要撰寫專用於 B 廠商資料庫之程式,十分的不方便。

使用 JDBC,可由廠商實作操作資料庫介面的驅動程式,而 Java 程式設計人員呼叫 JDBC 的 API 並操作 SQL,實際對資料庫的操作由 JDBC 驅動程式負責,如果要更換資料庫,基本上只要更換驅動程式,Java 程式中只要載入新的驅動程式來源即可完成資料庫的變更,Java 程式的部份則無需改變。

圖 20.1 是 JDBC API、資料庫驅動程式與資料庫之間的關係:

應用程式、JDBC 與驅動程式之間的關係

圖 20.1 應用程式、JDBC 與驅動程式之間的關係

簡單的說,JDBC 希望達到的目的,是讓 Java 程式設計人員在撰寫資料庫操作程式的時候,可以有個統一的操作介面,無需依賴於特定的資料庫 API,希望達到「寫一個 Java 程式,適用所有的資料庫」的目的。

JDBC 資料庫驅動程式依實作方式可以分為四個類型:

  • Type 1:JDBC-ODBC Bridge

    使用者的電腦上必須事先安裝好 ODBC 驅動程式,Type 1 驅動程式利用橋接(Bridge)方式,將 JDBC 的呼叫方式轉換為 ODBC 驅動程式的呼叫方式,例如 Microsoft Access 資料庫存取就是使用這種類型。

    Type 1: JDBC-ODBC Bridge

    圖 20.2 Type 1: JDBC-ODBC Bridge

  • Type 2:Native-API Bridge

    Type 1 驅動程式利用橋接方式,驅動程式上層包裝 Java 程式以與 Java 應用程式作溝通,將 JDBC 呼叫轉為原生(Native)程式碼的呼叫,下層為原生語言(像是 C、C++)來與資料庫作溝通,下層的函式庫是針對特定資料庫設計的,不若 Type 1 可以對 ODBC 架構的資料庫作存取。

    Type 2: Native-API Bridge

    圖 20.3 Type 2: Native-API Bridge

  • Type 3:JDBC-middleware

    透過中間件(middleware)來存取資料庫,使用者不必安裝特定的驅動程式,而是由驅動程式呼叫中間件,由中間件來完成所有的資料庫存取動作,然後將結果傳回給驅動程式。

    Type 3: JDBC-moddleware

    圖 20.4 Type 3: JDBC-moddleware

  • Type 4:Pure Java Driver

    使用純 Java 程式來撰寫驅動程式與資料庫作溝通,而不透過橋接或中間件來存取資料庫。

    Type 4: Pure Java Driver

    圖 20.5 Type 4: Pure Java Driver

在接下來的內容中,將使用 MySQL 資料庫系統進行操作,使用的 MySQL JDBC 驅動程式屬於 Type 4,您可以在以下的網址取得 MySQL 的 JDBC 驅動程式,這個章節中將使用 MySQL Connector/J 3.1

20.1.2 連接資料庫

為了要連接資料庫系統,您必須要有JDBC驅動程式,由於接下來將使用 MySQL 資料庫進行操作,所以請將下載回來的tar.gz檔案使用解壓縮軟體解開,並將當中的 mysql-connector-java-*.jar 加入至 Classpath 的設定之中,假設是放在 c:\workspace\library\mysql-connector-java-3.1.13-bin.jar,則 Classpath 中必須有 c:\workspace\library\mysql-connector-java-3.1.13-bin.jar 這個路徑設定。

在 Java SE 中與資料庫操作相關的 JDBC 類別都位於 java.sql 套件中,要連接資料庫,基本上必須有幾個動作:

  • 載入 JDBC 驅動程式

    首先必須先透過 java.lang.Class 類別的 forName(),動態載入驅動程式類別,並向 DriverManager 註冊 JDBC 驅動程式(驅動程式會自動透過 DriverManager.registerDriver() 方法註冊), MySQL 的驅動程式類別是 com.mysql.jdbc.Driver,一個載入 JDBC 驅動程式的程式片段如下所示:

    try {
        Class.forName("com.mysql.jdbc.Driver");
    }
    catch(ClassNotFoundException e) {
        System.out.println("找不到驅動程式類別");
    }
  • 提供JDBC URL

    JDBC URL 定義了連接資料庫時的協定、子協定、資料來源職別:

      協定:子協定:資料來源識別
    

    「協定」在 JDBC 中總是 jdbc 開始;「子協定」是橋接的驅動程式或是資料庫管理系統名稱,使用 MySQL 的話是 "mysql";「資料來源識別」標出找出資料庫來源的位址與連接埠。舉個例子來說,MySQL 的 JDBC URL 撰寫方式如下:

      jdbc:mysql://主機名稱:連接埠/資料庫名稱?參數=值&參數=值
    

    主機名稱可以是本機 localhost 或是其它連接主機,連接埠為 3306,假如要連接 demo 資料庫,並指明使用者名稱與密碼,可以如下指定:

      jdbc:mysql://localhost:3306/demo?user=root&password=123
    

    如果要使用中文存取的話,還必須給定參數 userUnicode 及 characterEncoding,表明是否使用 Unicode,並指定字元編碼方式,例如:

      jdbc:mysql://localhost:3306/demo?user=root&password=123&useUnicode=true&characterEncoding=Big5
    
  • 取得Connection

    要連接資料庫,可以向 java.sql.DriverManager 要求並取得 java.sql.Connection 物件,Connection 是資料庫連接的具體代表物件,一個 Connection 物件就代表一個資料庫連接,您可以使用 DriverManager 的 getConneciton() 方法,指定 JDBC URL 作為引數並取得 Connection 物件:

    try {
        String url =  "jdbc:mysql://localhost:3306/demo?" +
                       "user=root&password=123";
        Connection conn = DriverManager.getConnection(url);
        ....
    }
    catch(SQLException e) {
        ....
    }

    java.sql.SQLException 是在處理 JDBC 時很常遇到的一個例外物件,SQLException 是受檢例外(Checked Exception),您必須使用 try...catch 或 throws 明確處理,它表示 JDBC 操作過程中若發生錯誤時的具體物件代表。

取得 Connection 物件之後,可以使用 isClosed() 方法測試與資料庫的連接是否關閉,在操作完資料庫之後,若確定不再需要連接,則必須使用 close() 來關閉與資料庫的連接,以釋放連接時相關的必要資源。

getConnection() 方法可以在參數上指定使用者名稱與密碼,例如:

String url = "jdbc:mysql://localhost:3306/demo";
String user = "root";
String password = "123";
Connection conn = DriverManager.getConnection(url, user, password);

20.1.3 簡單的 Connection 工具類別

在之前示範取得 Connection 的程式片段中,您可以看到當中直接用字串在程式中寫下 JDBC URL、使用者名稱與密碼等資訊,實際的程式並不會將這些敏感資訊寫在程式碼之中,而且這麼做的話,如果要更改使用者名稱或密碼時,還要修改程式、重新編譯,在程式維護上並不方便。

您可以將 JDBC URL、使用者名稱與密碼等設定資訊,撰寫在一個屬性檔案當中,由程式讀取這個屬性檔中的資訊,如果需要變更資訊,則只要修改屬性檔即可,無須修改程式、重新編譯,在 Java SE 當中,屬性檔的讀取可以交給 java.util.Properties 類別。

舉個實際的例子,假設您使用了以下的指令在MySQL後建立了demo資料庫:

CREATE DATABASE demo;

由於取得 Connection 的方式,依所使用的環境及程式需求而有所不同,因而您可以先設計一個 DBSource 介面,規範取得 Connection 的方法,如範例 20.1 所示。

範例 20.1 DBSource.java

package onlyfun.caterpillar;

import java.sql.Connection;
import java.sql.SQLException;

public interface DBSource {
    public Connection getConnection() throws SQLException;
    public void closeConnection(Connection conn) throws SQLException;
}

接著可以實作 DBSource 介面,您的目的是從屬性檔案中讀取設定資訊、載入 JDBC 驅動程式,可以藉由 getConnection() 取得 Connection 物件,並藉由 closeConnection() 關閉 Connection 物件,在這邊以一個簡單的 SimpleDBSource 類別作為示範,如範例 20.2 所示。

範例 20.2 SimpleDBSource.java

package onlyfun.caterpillar;

import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;

public class SimpleDBSource implements DBSource {
    private Properties props;
    private String url;
    private String user;
    private String passwd;

    public SimpleDBSource() throws IOException, 
                                         ClassNotFoundException {
        this("jdbc.properties");
    }
	
    public SimpleDBSource(String configFile) throws IOException, 
                                                    ClassNotFoundException {
        props = new Properties();
        props.load(new FileInputStream(configFile));
		
        url = props.getProperty("onlyfun.caterpillar.url");
        user = props.getProperty("onlyfun.caterpillar.user");
        passwd = props.getProperty("onlyfun.caterpillar.password");
		
        Class.forName(
                    props.getProperty("onlyfun.caterpillar.driver"));
    }

    public Connection getConnection() throws SQLException {
        return DriverManager.getConnection(url, user, passwd);
    }

    public void closeConnection(Connection conn) throws SQLException {
        conn.close();
    }
}

預設的建構方法設定中,是讀取 jdbc.properties 檔案中的設定,如果打算自行指定屬性檔案名稱,則可以使用另一個有參數的建構方法。Properties 的 getProperty() 方法會讀取屬性檔案中的"鍵(Key)"對應的"值(Value)",假設您的屬性檔案設定如下:

範例 20.3 jdbc.properties

onlyfun.caterpillar.driver=com.mysql.jdbc.Driver
onlyfun.caterpillar.url=jdbc:mysql://localhost:3306/demo
onlyfun.caterpillar.user=root
onlyfun.caterpillar.password=123456

DBSource 的 getConnection() 簡單的從 DriverManager 的 getConnection() 方法取得 Connection 物件,而 closeConnection() 方法則是將給定的 Connection 關閉,就簡單的連接程式來說,這樣已經足夠,不過待會還會介紹連接池(Connection pool)的觀念,到時將會修改一下 DBSource 的 getConnection() 與 closeConnection() 方法,以達到重複使用 Connection,以簡省資源的目的。

最後,範例 20.4 使用一個簡單的程式來測試 SimpleDBSource 是否可以正確的取得與資料庫的連接,以及是否正確的關閉連接。

範例 20.4 ConnectionDemo.java

package onlyfun.caterpillar;

import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;

public class ConnectionDemo {
    public static void main(String[] args) {
        try {
            DBSource dbsource = new SimpleDBSource();
            Connection conn = dbsource.getConnection();
            
            if(!conn.isClosed()) {
                System.out.println("資料庫連接已開啟…");
            }
            
            dbsource.closeConnection(conn);
            
            if(conn.isClosed()) {
                System.out.println("資料庫連接已關閉…");
            }
            
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

如果您的 demo 資料庫已建立,並正確設定 jdbc.properties 中的相關資訊,則應該可以看到以下的執行結果:

資料庫連接已開啟…
資料庫連接已關閉…

20.1.4 簡單的連接池(Connection pool)

在資料庫應用程式中,資料庫連接的取得是一個耗費時間與資源的動作,包括了建立 Socket connection、交換資料(使用者密碼驗證、相關參數)、資料庫初始會話(Session)、日誌(Logging)、分配行程(Process)等資源。

如果資料庫的操作是很頻繁的動作,則要考慮到重複使用連接的需求,以節省在取得連接時的時間與資源,通常會實作一個連接池(Connection pool),有需要連接時可以從池中取得,不需要連接時就將連接放回池中,而不是直接關閉連接。

這邊將實作一個簡單的連接池,示範連接池中,重複使用連接的基本觀念,範例 20.5 使用 java.util.ArrayList 來實作連接池,可以將先前使用過的連接放到 ArrayList 物件中,下一次需要連接時則直接從 ArrayList 中取得。

範例 20.5 BasicDBSource.java

package onlyfun.caterpillar;

import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

public class BasicDBSource implements DBSource {
    private Properties props;
    private String url;
    private String user;
    private String passwd;
    private int max; // 連接池中最大Connection數目
    private List<Connection> connections;

    public BasicDBSource() throws IOException, ClassNotFoundException {
        this("jdbc.properties");
    }
    
    public BasicDBSource(String configFile) throws IOException, 
                                                     ClassNotFoundException {
        props = new Properties();
        props.load(new FileInputStream(configFile));
        
        url = props.getProperty("onlyfun.caterpillar.url");
        user = props.getProperty("onlyfun.caterpillar.user");
        passwd = props.getProperty("onlyfun.caterpillar.password");
        max = Integer.parseInt(
                   props.getProperty("onlyfun.caterpillar.poolmax"));
        Class.forName(
                   props.getProperty("onlyfun.caterpillar.driver"));
        
        connections = new ArrayList<Connection>();
    }

    public synchronized Connection getConnection() 
                                                    throws SQLException {
        if(connections.size() == 0) {
            return DriverManager.getConnection(url, user, passwd);
        }
        else {
            int lastIndex = connections.size() - 1;
            return connections.remove(lastIndex);
        }
    }
    
    public synchronized void closeConnection(Connection conn) 
                                                        throws SQLException {
        if(connections.size() == max) {
            conn.close();
        }
        else {
            connections.add(conn);
        }
    }
}

BasicDBSource 也是實作 DBSource 介面,考慮這個類別可能在多執行緒的環境中使用,因此在 getConnection() 與 closeConnection() 上使用 syhchronized 加以修飾。在取得連接時,如果目前池中沒有 Connection 物件,則新建立一個連接,如果有存在的 Connection 物件,則從池中移出。

BasicDBSource 可以設定連接池中最大 Connection 保存數量,如果超過這個數量,則傳入 closeConnection() 方法的 Connection 物件直接關閉,否則就放入連接池中,以在下一次需要資料庫連接時直接使用。範例 20.6 是個測試 BasicDBSource 的簡單程式。

範例 20.6 ConnectionPoolDemo.java

package onlyfun.caterpillar;

import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;

public class ConnectionPoolDemo {
    public static void main(String[] args) {
        try {
            DBSource dbsource = new BasicDBSource("jdbc2.properties");
            Connection conn1 = dbsource.getConnection();
            dbsource.closeConnection(conn1);
            Connection conn2 = dbsource.getConnection();
            System.out.println(conn1 == conn2);
            
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        
    }
}

這邊所使用的設定檔案是 jdbc2.properties,當中多了連接池最大數量之設定,如下所示:

範例 20.7 jdbc2.properties

onlyfun.caterpillar.driver=com.mysql.jdbc.Driver
onlyfun.caterpillar.url=jdbc:mysql://localhost:3306/demo
onlyfun.caterpillar.user=root
onlyfun.caterpillar.password=123456
onlyfun.caterpillar.poolmax=10

程式中取得 Connection 之後,將之使用 closeConnection() 關閉,但實際上 closeConnection() 並不是真正使用 close() 方法,而是放回池中,第二次取得 Connection 時,所取得的是先前放入池中的同一物件,因此執行的結果會顯示 true。

在更複雜的情況下,您還需要考慮到初始的 Connection 數量、Connection 最大 idle 的數量、如果超過多久時間,要回收多少數量的 Connection 等問題,實際上也不需要自行設計連接池的程式,現在網路上有不少優秀的開放原始碼連接池程式,例如 Proxool 或 Apache Jakarta 的 Common DBCP,您可以自行參考官方網站上的相關文件,了解它們各自是如何設定與使用。

20.2 使用 JDBC 進行資料操作

在了解如何使用 JDBC 進行資料庫的連接之後,接下來看看如何使用 JDBC 做資料庫基本操作,例如資料的新增、查詢等動作,而在這一個小節當中,也將稍微了解一下交易(Transaction)的基本觀念與操作。

20.2.1 Statement、ResultSet

前一個小節中提過,Connection 物件 Java 中資料庫連接的代表物件,接下來要執行 SQL 的話,必須取得 java.sql.Statement 物件,它是 Java 當中一個 SQL 敘述的具體代表物件,您可以使用 Connection 的 createStatement() 來建立 Statement 物件:

Statement stmt = conn.createStatement();

取得 Statement 物件之後,可以使用 executeUpdate()、executeQuery() 等方法來執行 SQL,executeUpdate() 主要是用來執行 CREATE TABLE、INSERT、DROP TABLE、ALTER TABLE 等會改變資料庫內容的 SQL,例如可以在 demo 資料庫中建立一個 t_message 表格:

Use demo;
CREATE TABLE t_message ( 
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, 
    name CHAR(20) NOT NULL, 
    email CHAR(40), 
    msg TEXT NOT NULL 
);

如果要在這個表格中插入一筆資料,可以如下使用 Statement 的 executeUpdate() 方法:

stmt.executeUpdate("INSERT INTO t_message VALUES(1, 'justin', " +
       "'justin@mail.com', 'mesage...')");

Statement 的 executeQuery() 方法則是用於 SELECT 等查詢資料庫的 SQL,executeUpdate() 會傳回 int 結果,表示資料變動的筆數,executeQuery() 會傳回 java.sql.ResultSet 物件,代表查詢的結果,查詢的結果會是一筆一筆的資料。可以使用 ResultSet 的 next() 來移動至下一筆資料,它會傳回 true 或 false 表示是否有下一筆資料,接著可以使用 getXXX() 來取得資料,例如 getString()、getInt()、getFloat()、getDouble() 等方法,分別取得相對應的欄位型態資料,getXXX() 方法都提供有依欄位名稱取得資料,或是依欄位順序取得資料的方法,一個例子如下,您指定欄位名稱來取得資料:

ResultSet result =
          stmt.executeQuery("SELECT * FROM t_message");
while(result.next()) {
    System.out.print(result.getInt("id") + "\t");
    System.out.print(result.getString("name") + "\t");
    System.out.print(result.getString("email") + "\t");
    System.out.print(result.getString("msg") + "\t");
}

使用查詢結果的欄位順序來顯示結果的方式如下:

ResultSet result =
          stmt.executeQuery("SELECT * FROM t_message");
while(result.next()) {
    System.out.print(result.getInt(1) + "\t");
    System.out.print(result.getString(2) + "\t");
    System.out.print(result.getString(3) + "\t");
    System.out.print(result.getString(4) + "\t");
}

Statement 的 execute() 可以用來執行 SQL,並可以測試所執行的 SQL 是執行查詢或是更新,傳回 true 的話表示 SQL 執行將傳回 ResultSet 表示查詢結果,此時可以使用 getResultSet() 取得 ResultSet 物件,如果 execute() 傳回 false,表示 SQL 執行會傳回更新筆數或沒有結果,此時可以使用 getUpdateCount() 取得更新筆數。如果事先無法得知是進行查詢或是更新,就可以使用 execute()。

範例 20.8 是個示範新增與查詢資料的範例,當中使用了前一節設計的 SimpleDBSource。注意在查詢結束後,要使用 Statement 的 close() 方法來釋放 Statement 的資源,而最後不使用連接時,也使用了 closeConnection() 來關閉連接。

範例 20.8 StatementResultDemo.java

package onlyfun.caterpillar;

import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class StatementResultDemo {
    public static void main(String[] args) {
        DBSource dbsource = null;
        Connection conn = null;
        Statement stmt = null;
        try {
            dbsource = new SimpleDBSource();
            conn = dbsource.getConnection();
            
            stmt = conn.createStatement(); 

            stmt.executeUpdate(
               "INSERT INTO t_message VALUES(1, 'justin', " +
               "'justin@mail.com', 'mesage...')");
 
            ResultSet result = stmt.executeQuery(
                                  "SELECT * FROM t_message"); 
            while(result.next()) { 
                System.out.print(result.getInt(1) + "\t"); 
                System.out.print(result.getString(2) + "\t"); 
                System.out.print(result.getString(3) + "\t"); 
                System.out.println(result.getString(4)); 
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        finally {
            if(stmt != null) {
                try {
                    stmt.close();
                }   
                catch(SQLException e) {
                    e.printStackTrace();
                }
            }
            if(conn != null) {
                try {
                    dbsource.closeConnection(conn);
                }
                catch(SQLException e) {
                    e.printStackTrace();
                }
            }
        } 
    }
}

如果您依之前的 SQL 在 demo 資料庫中建立了 t_message 表格,則執行之後會在 t_message 表格中新增一筆資料,接著查詢 t_message 表格中的資料並加以顯示如下:

1	justin	justin@mail.com	mesage...

最後注意到的是,Connection 物件預設為「自動認可」(auto commit),也就是 Statement 執行 SQL 敘述完後,馬上對資料庫進行操作變更,如果想要對 Statement 要執行的 SQL 進行除錯,可以使用 setAutoCommit(false) 來將自動認可取消,在執行完 SQL 之後,再呼叫 Connection 的 commit() 方法認可變更,使用 Connection 的 getAutoCommit() 可以測試是否設定為自動認可。不過無論是否有無執行 commit() 方法,只要 SQL 沒有錯,在關閉 Statement 或 Connection 前,都會執行認可動作,對資料庫進行變更。

良葛格的話匣子 之後還會談到,使用 setAutoCommit(false),是進行交易(Transaction)前的一個必要動作。

20.2.2 PreparedStatement

Statement 主要用於執行靜態的 SQL 陳述,也就是在執行 executeQuery()、executeUpdate() 等方法時,指定內容固定不變的 SQL 語句字串,每一句 SQL 只適用於當時的執行,如果您有些操作只是 SQL 語句當中某些參數會有所不同,其餘的 SQL 子句皆相同,則您可以使用 java.sql.PreparedStatement。

您可以使用 Connection 的 preparedStatement() 方法建立好一個預先編譯(precompile)的 SQL 語句,當中參數會變動的部份,先指定 "?" 這個佔位字元,例如:

PreparedStatement stmt = conn.prepareStatement(
                "INSERT INTO t_message VALUES(?, ?, ?, ?)");

等到需要真正指定參數執行時,再使用相對應的 setInt()、setString() 等方法,指定 "?" 處真正應該有的參數,例如:

stmt.setInt(1, 2);
stmt.setString(2, "momor");
stmt.setString(3, "momor@mail.com");
stmt.setString(4, "message2...");

所以使用 PreparedStatement,可以讓您先準備好一段 SQL,並重複使用這段 SQL 語句,範例 20.9 改寫自範例 20.8,使用 PreparedStatement 來插入兩筆資料,而您只要準備一次 SQL 語句就可以了:

範例 20.9 PreparedStatementDemo.java

package onlyfun.caterpillar;

import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.PreparedStatement;

public class PreparedStatementDemo {
    public static void main(String[] args) {
        DBSource dbsource = null;
        Connection conn = null;
        PreparedStatement stmt = null;
        try {
            dbsource = new SimpleDBSource();
            conn = dbsource.getConnection();
            
            stmt = conn.prepareStatement(
                    "INSERT INTO t_message VALUES(?, ?, ?, ?)");
            
            stmt.setInt(1, 2);
            stmt.setString(2, "momor");
            stmt.setString(3, "momor@mail.com");
            stmt.setString(4, "message2...");
            
            stmt.executeUpdate();
            stmt.clearParameters();
            
            stmt.setInt(1, 3);
            stmt.setString(2, "bush");
            stmt.setString(3, "bush@mail.com");
            stmt.setString(4, "message3...");
            
            stmt.executeUpdate();
            stmt.clearParameters();

            ResultSet result = stmt.executeQuery(
                                  "SELECT * FROM t_message"); 
            while(result.next()) { 
                System.out.print(result.getInt(1) + "\t"); 
                System.out.print(result.getString(2) + "\t"); 
                System.out.print(result.getString(3) + "\t"); 
                System.out.println(result.getString(4)); 
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        finally {
            if(stmt != null) {
                try {
                    stmt.close();
                }   
                catch(SQLException e) {
                    e.printStackTrace();
                }
            }
            if(conn != null) {
                try {
                    dbsource.closeConnection(conn);
                }
                catch(SQLException e) {
                    e.printStackTrace();
                }
            }
        } 
    }
}

setXXX() 方法的第一個參數指定"?"的位置,而第二個參數為要新增至資料表欄位的值,要讓 SQL 執行生效,要執行 executeQuery() 或 executeUpdate() 方法,使用 setXXX() 來設定的參數會一直有效,可以於下一次使用,如果想要清除設定好的參數,可以執行 clearParameters() 方法。以下是這個範例的執行結果參考:

1	justin	justin@mail.com	mesage...
2	momor	momor@mail.com	message2...
3	bush	bush@mail.com	message3...

20.2.3 LOB 讀寫

如果將要檔案寫入資料庫,您可以在表格欄位上使用 BLOB 或 CLOB 資料型態,BLOB 全名 Binary Large Object,用於儲存大量的二進位資料,CLOB 全名 Character Large Object,用於儲存大量的文字資料。

在 JDBC 中也提供了 java.sql.Blob 與 java.sql.Clob 兩個類別分別代表 BLOB 與 CLOB 資料,您可以使用 PreparedStatement 的 setBinaryStream()、 setObject()、setAsciiStream()、setUnicodeStream() 等方法來代替,例如可以如下取得一個檔案,並將之存入資料庫中:

// 取得檔案
File file = new File("./logo_phpbb.jpg");
int length = (int) file.length();
InputStream fin = new FileInputStream(file);
 // 填入資料庫
PreparedStatement pstmt = conn.prepareStatement(
                       "INSERT INTO files VALUES(?, ?, ?)");
pstmt.setInt(1, 1);
pstmt.setString(2, "filename");
pstmt.setBinaryStream (3, fin, length);
pstmt.executeUpdate();
pstmt.clearParameters();
pstmt.close();
fin.close();

如果要從資料庫中取得 BLOB 或 CLOB 資料,您可以如下進行,其中 result 參考一個 ResultSet 的實例:

Blob blob = result.getBlob(2);  // 取得BLOB
Clob clob = result.getClob(2)  // 取得CLOB

Blob 擁有 getBinaryStream()、getBytes() 等方法,可以取得二進位串流或 byte 等資料,同樣的,Clob 擁有 getCharacterStream()、getSubString() 等方法,可以取得字元串流或子字串等資料,您可以查看 API 文件來獲得更詳細的訊息。

接下來舉個完整的範例,假設您使用以下的 SQL 建立了 t_file 表格,當中的 BLOB 欄位將儲入指定的檔案:

CREATE TABLE t_file ( 
    id INT(11) NOT NULL auto_increment PRIMARY KEY,
    filename VARCHAR(100) NOT NULL,
    file BLOB
);

範例 20.10 示範將檔案存入資料庫,而後查詢出資料並另存新檔的基本流程:

範例 20.10 LobDemo.java

package onlyfun.caterpillar;

import java.io.*; 
import java.sql.*;

public class LobDemo {    
    public static void main(String[] args) {
        DBSource dbsource = null;
        Connection conn = null;
        PreparedStatement pstmt = null;
        
        try {
            dbsource = new SimpleDBSource();
            conn = dbsource.getConnection(); 

            // 取得檔案
            File file = new File(args[0]); 
            int length = (int) file.length(); 
            InputStream fin = new FileInputStream(file); 

            // 填入資料庫
            pstmt = conn.prepareStatement( 
                "INSERT INTO t_file VALUES(?, ?, ?)");
            pstmt.setInt(1, 1);
            pstmt.setString(2, args[0]); 
            pstmt.setBinaryStream (3, fin, length); 
            pstmt.executeUpdate(); 
            pstmt.clearParameters(); 

            fin.close(); 
        }
        catch(SQLException e) { 
            e.printStackTrace(); 
        }  
        catch(IOException e) { 
            e.printStackTrace(); 
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } 
        finally {
            if(pstmt != null) {
                try {
                    pstmt.close();
                }   
                catch(SQLException e) {
                    e.printStackTrace();
                }
            }
        } 

        Statement stmt = null;
        
        try {
            // 從資料庫取出檔案
            stmt = conn.createStatement(); 
            ResultSet result = stmt.executeQuery(
                "SELECT * FROM t_file"); 
            result.next(); 
            String filename = result.getString(2); 
            Blob blob = result.getBlob(3); 

            // 寫入檔案,檔名附加.bak
            FileOutputStream fout = 
                new FileOutputStream(filename + ".bak"); 
            fout.write(blob.getBytes(1, (int)blob.length())); 
            fout.flush(); 
            fout.close();
        } 
        catch(SQLException e) { 
            e.printStackTrace(); 
        } 
        catch(IOException e) { 
            e.printStackTrace(); 
        } 
        finally {
            if(stmt != null) {
                try {
                    stmt.close();
                }   
                catch(SQLException e) {
                    e.printStackTrace();
                }
            }
            if(conn != null) {
                try {
                    dbsource.closeConnection(conn);
                }
                catch(SQLException e) {
                    e.printStackTrace();
                }
            }
        } 
    }
} 

這個程式會將您於命令列引數所指定的檔案儲存至資料庫的 t_file 表格中,並將檔案路徑名稱也一併存入,之後從 t_file 中查詢出資料時,根據先前存入的檔案路徑名稱,將檔案另存為 .bak 結尾的名稱。

良葛格的話匣子 Lob 的讀寫方法在 Oracle 中有所不同,您可以參考:

在 Java SE 6 中,對於 Blob、Clob 做了改進,詳細可以參考第 21 章的內容。

20.2.5 交易(Transaction)

交易是一組原子(Atomic)操作(一組 SQL 執行)的工作單元,這個工作單元中的所有原子操作在進行期間,與其它交易隔離,免於數據來源的交相更新而發生混亂,交易中的所有原子操作,要嘛全部執行成功,要嘛全部失敗(即使只有一個失敗,所有的原子操作也要全部撤消)。

舉個簡單的例子,一個客戶從 A 銀行轉帳至 B 銀行,要做的動作為從A銀行的帳戶扣款、在 B 銀行的帳戶加上轉帳的金額,兩個動作必須成功,如果有一個動作失敗,則此次轉帳失敗。

在 JDB C中,可以操作 Connection 的 setAutoCommit() 方法,給它 false 引數,在下達一連串的 SQL 語句後,自行呼叫 Connection 的 commit() 來送出變更,如果中間發生錯誤,則呼叫 rollback() 來撤消所有的執行,一個示範的流程如下所示:

try { 
    …
    conn.setAutoCommit(false); // 設定auto commit為false

    stmt = conn.createStatement(); 
    stmt.execute("...."); // SQL
    stmt.execute("....");
    stmt.execute("....");
    
    conn.commit(); // 正確無誤,確定送出
 } 
 catch(SQLException e) { // 喔喔!在commit()前發生錯誤
    try { 
         conn.rollback(); // 撤消操作
    } catch (SQLException e1) {
         e1.printStackTrace();
    }
    e.printStackTrace(); 
 }

如果您在交易管理時,僅想要撤回(rollback)某個SQL執行點,則您可以設定儲存點(save point),例如:

conn.setAutoCommit(false);
Statement stmt = conn.createStatement();
stmt.executeUpdate("....");
stmt.executeUpdate("....");
Savepoint savepoint = conn.setSavepoint(); // 設定save point
stmt.executeUpdate("....");
// 如果因故rollback
conn.rollback(savepoint);
. . .
conn.commit();
// 記得釋放save point
stmt.releaseSavepoint(savepoint);

良葛格的話匣子 您的資料表格必須支援交易,才可以執行以上所提到的功能,例如在 MySQL 中可以建立 InnoDB 類型的表格:

CREATE TABLE t_xxx (
    ... 
) Type = InnoDB;

實際的交易還有非常多要考量的因素,實際撰寫專案時,常會依賴於持久層框架所提供的交易管理機制,以獲得更多有關交易的完善功能。

20.2.6 批次處理

Statement 的 execute 等方法一次只能執行一個 SQL 敘述,如果有多個 SQL 敘述要執行的話,可以使用 executeBatch() 方法,在一次方法呼叫中執行多個 SQL 敘述,以增加執行的效能,您可以使用 addBatch() 方法將要執行的 SQL 敘述加入,然後執行 executeBatch() 即可:

conn.setAutoCommit(false);
Statement stmt = conn.createStatement();
stmt.addBatch("..."); // SQL
stmt.addBatch("...");
stmt.addBatch("...");
...
stmt.executeBatch();
conn.commit();

在執行 executeBatch() 時而 SQL 有錯誤的情況下,會丟出 BatchUpdateException 例外,您可以由這個例外物件的 getUpdateCounts() 方法取得發生錯誤的SQL句數,如果中間有個 SQL 執行錯誤,則應該撤回(rollback)整個批次處理過程的SQL操作。

使用 PreparedStatement 也可以進行批次處理,直接來看個例子:

PreparedStatement stmt = conn.prepareStatement(
    "INSERT INTO t_message VALUES(?, ?, ?, ?)");
Message[] messages = ...;

for(int i = 0; i < messages.length; i++) {
     stmt.setInt(1, messages[i].getID());
     stmt.setString(2, messages[i].getName());
     stmt.setString(3, messages[i].getEmail());
     stmt.setString(4, messages[i].getMsg());
     stmt.addBatch();
}
 
stmt.executeBatch(); 

20.2.7 ResultSet 游標控制

在建立 Statement 或 PreparedStatement 時,您所使用的是 Connection 無參數的 createStatement(),或是僅指定預編譯的 SQL 之 preparedStatement(),這樣取得的 Statement 或 PreparedStatement,在執行 SQL 後所得到的 ResultSet,將只能使用 next() 方法逐筆取得查詢結果。

您可以在建立 Statement 物件時指定 resultSetType,可指定的參數有 ResultSet.TYPE_FORWARD_ONLY、ResultSet.TYPE_SCROLL_INSENSITIVE 與 ResultSet.TYPE_SCROLL_SENSITIVE,在不指定的情況下,預設是第一個,也就是只能使用 next() 來逐筆取得資料,指定第二個或第三個時,則可以使用 ResultSet 的 afterLast()、previous()、absolute()、relative() 等方法來移動以取得資料。

TYPE_SCROLL_INSENSITIVE 與 TYPE_SCROLL_SENSITIVE 的差別,在於能否取得 ResultSet 改變值後的資料,另外您還必須指定 resultSetConcurrency,有 ResultSet.CONCUR_READ_ONLY 與 ResultSet.CONCUR_UPDATABLE 兩個參數可以設定,前者表示只能讀取 ResultSet 的資料,後者表示可以直接使用 ResultSet 來操作資料庫,這會在下一個主題後說明。

createStatement() 不給定參數時,預設是 TYPE_FORWARD_ONLY、 CONCUR_READ_ONLY。

這邊先示範如何控制 ResultSet 的讀取游標,在建立 Statement 時,使用 TYPE_SCROLL_INSENSITIVE 及 CONCUR_READ_ONLY 即可,範例 20.11 示範從查詢到的資料最後一筆開始往前讀取:

範例 20.11 ResultDemo.java

package onlyfun.caterpillar;

import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class ResultDemo {
    public static void main(String[] args) {
        DBSource dbsource = null;
        Connection conn = null;
        Statement stmt = null;
        try {
            dbsource = new SimpleDBSource();
            conn = dbsource.getConnection();
            
            stmt = conn.createStatement(
                            ResultSet.TYPE_SCROLL_INSENSITIVE, 
                            ResultSet.CONCUR_READ_ONLY); 
 
            ResultSet result = stmt.executeQuery(
                                  "SELECT * FROM t_message");
            result.afterLast(); 
            
            while(result.previous()) { 
                System.out.print(result.getInt("id") + "\t"); 
                System.out.print(result.getString("name") + "\t"); 
                System.out.print(result.getString("email") + "\t"); 
                System.out.println(result.getString("msg")); 
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        finally {
            if(stmt != null) {
                try {
                    stmt.close();
                }   
                catch(SQLException e) {
                    e.printStackTrace();
                }
            }
            if(conn != null) {
                try {
                    dbsource.closeConnection(conn);
                }
                catch(SQLException e) {
                    e.printStackTrace();
                }
            }
        } 
    }
} 

afterLast() 會將 ResultSet 的讀取游標移至最後一筆資料之後,您使用 previous() 方法往前移動讀取游標,執行的結果如下所示:

3	bush	bush@mail.com	message3...
2	momor	momor@mail.com	message2...
1	justin	justin@mail.com	mesage....

您也可以使用 absolute() 方法指定查詢到的資料之位置,例如 absolute(4) 表示第四筆資料,absoulte(10) 則是第十筆資料,如果指定負數,則從最後往前數,例如 absolute(-1) 則是最後一筆資料,若有 100 筆資料,absoulte(-4) 則是第 97 筆資料。

relative() 方法則從目前游標處指定相對位置,例如若目前在第 25 筆資料,則 relative(-2) 則表示第 23 筆資料,而 relative(4) 則表示第 29 筆資料。另外還有 beforeFirst(),可以將游標移至資料的第一筆之前,first() 可以將游標移至第一筆資料,而 last() 可以將游標移至最後一筆資料。

20.2.8 ResultSet 新增、更新、刪除資料

之前要進行新增、更新或刪除資料,都必須要撰寫 SQL,然後使用 executeUpdate() 來執行 SQL,將 SQL 寫在 executeUpdate() 之中,其實是麻煩又容易出錯的動作,如果只是想要針對查詢到的資料進行一些簡單的新增、更新或刪除資料,可以藉由 ResultSet 的一些方法來執行,而不一定要撰寫 SQL 並執行。

想要使用 ResultSet 直接進行新增、更新或刪除資料,在建立 Statement 時必須在 createStatement() 上指定 TYPE_SCROLL_SENSITIVE(或 TYPE_SCROLL_INSENSITIVE,如果不想取得更新後的資料的話)與 CONCUR_UPDATABLE,例如:

Statement stmt = conn.createStatement(
                        ResultSet.TYPE_SCROLL_SENSITIVE, 
                        ResultSet.CONCUR_UPDATABLE);

假如想要針對查詢到的資料進行更新的動作,則先移動游標至想要更新的資料位置,然後使用 updateXXX() 等對應的方法即可,最後記得使用 updateRow() 讓更新生效,例如:

ResultSet result = stmt.executeQuery(
        "SELECT * FROM t_message WHERE name='justin'");
result.last();
result.updateString("name", "caterpillar");
result.updateString("email", "caterpillar@mail.com");
result.updateRow();

使用 updateXXX() 等方法之後,並不會馬上對資料庫生效,而必須執行完 updateRow() 方法才會對資料庫進行操作,如果在 updateRow() 前想要取消之前的 updateXXX() 方法,則可以使用 cancelRowUpdates() 方法取消。

如果想要新增資料,則先使用 moveToInsertRow() 移至新增資料處,執行相對的 updateXXX() 方法,然後再執行 insertRow () 即可新增資料,例如:

ResultSet result = stmt.executeQuery(
        "SELECT * FROM t_message WHERE name='caterpillar'");
result.moveToInsertRow();
result.updateInt("id", 4);
result.updateString("name", "jazz");
result.updateString("email", "jazz@mail.com");
result.updateString("msg", "message4...");
result.insertRow();

如果想要刪除查詢到的某筆資料,則可以將游標移至該筆資料,然後執行 deleteRow() 方法即可:

ResultSet result = stmt.executeQuery(
        "SELECT * FROM t_message WHERE name='caterpillar'");
result.last();
result.deleteRow();

20.2.9 ResultSetMetaData

Meta Data 即「資料的資料」(Data about data),ResultSet 用來表示查詢到的資料,而 ResultSe t資料的資料,即描述所查詢到的資料背後的資料描述,即用來表示表格名稱、欄位名稱、欄位型態等,這些訊息可以透過 ResultSetMetaData 來取得。

範例 20.12 直接示範如何取得查詢到的資料欄位數、表格名稱、欄位名稱與欄位資料型態:

範例 20.12 ResultSetMetaDataDemo.java

package onlyfun.caterpillar;
 
import java.io.IOException;
import java.sql.*; 
 
public class ResultSetMetaDataDemo { 
    public static void main(String[] args) {
        DBSource dbsource = null;
        Connection conn = null;
        Statement stmt = null;

        try { 
            dbsource = new SimpleDBSource();
            conn =  dbsource.getConnection();
 
            stmt = conn.createStatement(); 
            ResultSet result = stmt.executeQuery(
                                  "SELECT * FROM t_message"); 
            ResultSetMetaData metadata = 
                                  result.getMetaData(); 
 
            for(int i = 1; i <= metadata.getColumnCount(); i++) { 
                System.out.print(
                        metadata.getTableName(i) + "."); 
                System.out.print(
                        metadata.getColumnName(i) + "\t|\t"); 
                System.out.println(
                        metadata.getColumnTypeName(i)); 
            } 
        } 
        catch(SQLException e) { 
            e.printStackTrace(); 
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } 
        finally {
            if(stmt != null) {
                try {
                    stmt.close();
                }   
                catch(SQLException e) {
                    e.printStackTrace();
                }
            }
            if(conn != null) {
                try {
                    dbsource.closeConnection(conn);
                }
                catch(SQLException e) {
                    e.printStackTrace();
                }
            }
        } 
    } 
} 

執行的結果如下所示:

t_message.id	|	INTEGER
t_message.name	|	CHAR
t_message.email	|	CHAR
t_message.msg	|	VARCHAR

20.3 接下來的主題

每一個章節的內容由淺至深,初學者該掌握的深度要到哪呢?在這個章節中,對於初學者我建議至少掌握以下幾點內容:

  • JDBC 的基本架構
  • 知道如何建立與資料庫的連線
  • 會使用 Statement、ResultSet 查詢資料
  • PreparedStatement 與 Statement 的差別與使用

Java SE 6 多了不少新的功能,對於本書中有提到的主題,而 Java SE 6 中又有相對應的更新,在下一個章節中會加以介紹,在 JDBC 這方面,Java SE 6 中新的 JDBC 4.0 也有許多不錯的新增功能,這都將在下個章節為您說明。

Morty Proxy This is a proxified and sanitized view of the page, visit original site.