20230628 5. 數據庫編程

數據庫編程

Java 數據庫連接( JDBC )API 使編程人員可以通過這個 API 接口連接到數據庫,並使用結構化查詢語 (即 SQL )完成對數據庫的查找與更新

根據 Oracle 的聲明,JDBC 是一個註冊了商標的術語,而並非 Java Database Connectivity 的首字母縮寫。對它的命名體現了對 ODBC 的致敬,後者是微軟開創的標準數據庫 API ,並因此而併入了 SQL 標準中

JDBC 規範下載地址

JDBC 的設計

業界存在許多不同的數據庫,且它們所使用的協議也各不相同。儘管很多數據庫供應商都表示支持 Java 提供一套數據庫訪問的標準網絡協議,但是每家企業都希望 Java 能採用自己的網絡協議

所有的數據庫供應商和工具開發商都認爲,如果 Java 能夠爲 SQL 訪問提供一套 “純” JavaAPI ,同時提供一個驅動管理器,以允許第三方驅動程序可以連接到特定的數據庫,那它就會顯得非常有用。這樣,數據庫供應商就可以提供自己的驅動程序,將其插入到驅動管理器中。這將成爲一種向驅動管理器註冊第三方驅動程序的簡單機制

這種接口組織方式遵循了微軟公司非常成功的 ODBC 模式, ODBC 爲 C 語言訪問數據庫提供了一套編程接口。 JDBC 和 ODBC 都基於同一個思想:根據 API 編寫的程序都可以與驅動管理器進行通信,而驅動管理器則通過驅動程序與實際的數據庫進行通信

所有這些都意味着 JDBC API 是大部分程序員不得不使用的接口

JDBC 驅動程序類型

JDBC 規範將驅動程序歸結爲以下幾類:

  • 第一類驅動程序將 JDBC 翻譯成 ODBC ,然後使用一個 ODBC 驅動程序與數據庫進行通信。較早版本的 Java 包含了一個這樣的驅動程序: JDBC/ODBC 橋,不過在使用這個橋接器之前需要對 ODBC 進行相應的部署和正確的設置。 JDBC 面世之初,橋接器可以方便地用於測試,卻不太適用於產品的開發。 Java 8 已經不再提供 JDBC/ODBC 橋了
  • 第二類驅動程序是由部分 Java 程序和部分本地代碼組成的,用於與數據庫的客戶端 API 進行通信。在使用這種驅動程序之前,客戶端不僅需要安裝 Java 類庫,還需要安裝一些與平臺相關的代碼
  • 第三類驅動程序是純 Java 客戶端類庫,它使用一種與具體數據庫無關的協議將數據庫請求發送給服務器構件,然後該構件再將數據庫請求翻譯成數據庫相關的協議。這簡化了部署,因爲平臺相關的代碼只位於服務器端
  • 第四類驅動程序是純 Java 類庫,它將 JDBC 請求直接翻譯成數據庫相關的協議

大部分數據庫供應商都爲他們的產品提供第三類或第四類驅動程序

JDBC 最終是爲了實現以下目標:

  • 通過使用標準的 SQL 語句,甚至是專門的 SQL 擴展,程序員就可以利用 Java 語言開發訪問數據庫的應用,同時還依舊遵守 Java 語言的相關約定
  • 數據庫供應商和數據庫工具開發商可以提供底層的驅動程序。因此,他們可以優化各自數據庫產品的驅動程序

JDBC 的典型用法

JDBC 驅動程序應該部署在客戶端

img

結構化查詢語言( SQL )

JDBC 使得我們可以通過 SQL 與數據庫進行通信

我們可以將 JDBC 包看作是一個用於將 SQL 語句傳遞給數據庫的應用編程接口( API )

JDBC 配置

數據庫 URL

JDBC 使用了一種與普通 URL 相類似的語法來描述數據源 下面是這種語法的兩個實例:

jdbc:derby://localhost:1527/COREJAVA;create=true
jdbc:postgresql:COREJAVA

JDBC URL 般語法爲:

jdbc:subprotocol:other stuff

其中, subprotocol 用於選擇連接到數據庫的具體驅動程序

other stuff 參數的格式隨所使用的 subprotocol 不同而不同

驅動程序 JAR 文件

在運行訪問數據庫的程序時,需要將驅動程序的 JAR 文件包括到類路徑中(編譯時並不需要這個 JAR 文件)

註冊驅動器類

許多 JDBC JAR 文件會自動註冊驅動器類,在這種情況下,可以跳過手動註冊步驟。包含 META-INF/services/java.sql.Driver 文件的 JAR 文件可以自動註冊驅動器類,解壓縮驅動程序 JAR 文件就可以檢查其是否包含該文件

自動註冊對於遵循 JDBC4 的驅動程序是必須具備的特性,參看 官方文檔

如果驅動程序 JAR 文件不支持自動註冊,那就需要找出數據庫提供商使用的 JDBC 驅動器類的名字。典型的驅動器名字如下:

com.mysql.cj.jdbc.Driver

調試與 JDBC 相關的問題時,有種方法是啓用 JDBC 的跟蹤特性。調用 DriverManager.setLogWriter 方法可以將跟蹤信息發送給 PrintWriter ,而 PrintWriter 將輸出 JDBC 活動的詳細列表

PrintWriter printWriter = new PrintWriter(System.out);
DriverManager.setLogWriter(printWriter);

Connection connection = DriverManager.getConnection(url, username, password);

示例程序:

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

public class TestDB {
    public static void main(String args[]) throws IOException {
        try {
            runTest();
        } catch (SQLException ex) {
            for (Throwable t : ex) {
                t.printStackTrace();
            }
        }
    }


    public static void runTest() throws SQLException, IOException {

        try (Connection conn = getConnection();
             Statement stat = conn.createStatement()) {
            stat.executeUpdate("CREATE TABLE Greetings (Message CHAR(20))");
            stat.executeUpdate("INSERT INTO Greetings VALUES ('Hello, World!')");

            try (ResultSet result = stat.executeQuery("SELECT * FROM Greetings")) {
                if (result.next()) {
                    System.out.println(result.getString(1));
                }
            }
            stat.executeUpdate("DROP TABLE Greetings");
        }
    }

    public static Connection getConnection() throws SQLException, IOException {
        Properties props = new Properties();
        try (InputStream in = Files.newInputStream(Paths.get("database.properties"))) {
            props.load(in);
        }
        String drivers = props.getProperty("jdbc.drivers");
        if (drivers != null) {
            System.setProperty("jdbc.drivers", drivers);
        }
        String url = props.getProperty("jdbc.url");
        String username = props.getProperty("jdbc.username");
        String password = props.getProperty("jdbc.password");

        PrintWriter printWriter = new PrintWriter(System.out);
        DriverManager.setLogWriter(printWriter);

        Connection connection = DriverManager.getConnection(url, username, password);

        return connection;
    }
}
java.sql.DriverManager 方法名稱 方法聲明 描述
getConnection public static Connection getConnection(String url, String user, String password) throws SQLException 建立一個到指定數據庫的連接,並返回一個 Connection 對象

使用 JDBC 語句

執行 SQL 語句

在執行 SQL 語句之前,首先需要創建一個 Statement 對象。要創建 Statement 對象,需要使用調用 DriverManager.getConnection 方法所獲得的 Connection 對象

Statement stat = conn.createStatement()

Statement 對象的 executeUpdate 方法將返回受 SQL 語句影響的行數,或者對不返回行數的語句返回 0

executeUpdate 法既可以執行諸如 INSERTUPDATEDELETE 類的操作,也可以執行諸如 CREATE TABLEDROP TABLE 類的數據定義語句 但是,執行 SELECT 查詢時必須使用 executeQuery 方法.另外還有一個 execute 語句可以執行任意的 SQL 語句,此方法通常只用於由用戶提供的交互式查詢

當我們執行查詢操作時,通常感興趣的是查詢結果。 executeQuery 方法會返回 ResultSet 類型的對象,可以通過它來每次一行地迭代遍歷所有查詢結果

try (ResultSet result = stat.executeQuery("SELECT * FROM Greetings")) {
    while (result.next()) {
        System.out.println(result.getString(1));
    }
}

ResultSet 接口的迭代協議與 java.util.Iterator 接口稍有不同。對於 ResultSet 接口,迭代器初始化時被設定在第一行之前的位置,必須調用 next 方法將它移動到第一行。另外,它沒有 hasNext 方法,我們需要不斷地調用 next 直至該方法返回 false

結果集中行的順序是任意排列的。除非使用 ORDER BY 子句指定行的順序,否則不能爲行序強加任何意義

查看每一行時,可能希望知道其中每一列的 容,有許多訪問器( accessor )方法( getXxx )可以用於獲取這些信息。不同的數據類型有不同的訪問器,比如 getStringgetDouble 。每個訪問器都有兩種形式,一種接受數字型參數,另一種接受字符串參數。當使用數字型參數時,我們指的是該數字所對應的列。例如, rs.getString(1) 返回的是當前行中第一列的值

與數組的索引不同,數據庫的列序號是從 1 開始計算的

當使用字符串參數時,指的是結果集中以該字符串爲列名的列。例如, rs.getDouble("Price") 返回列名爲 Price 的列所對應的值。使用數字型參數效率更高一些,但是使用字符串參數可以使代碼易於閱讀和維護

get 方法的類型和列的數據類型不一致時,每個 get 方法都會進行合理的類型轉換。例如,調用 rs.getString("Price") 時,該方法會將 Price 列的浮點值轉換成字符串

java.sql.Connection 方法名稱 方法聲明 描述
createStatement Statement createStatement() throws SQLException; 創建一個 Statement 對象,用以執行不帶參數的 SQL 查詢和更新
close void close() throws SQLException; 立即關閉當前的連接,並釋放由它所創建的 JDBC 資源
java.sql.Statement 方法名稱 方法聲明 描述
executeQuery ResultSet executeQuery(String sql) throws SQLException; 執行給定字符串中的 SQL 語句,並返回一個用於查看查詢結果的 ResultSet 對象
executeUpdate
executeLargeUpdate
int executeUpdate(String sql) throws SQLException;
default long executeLargeUpdate(String sql) throws SQLException
執行字符串中指定的 INSERTUPDATEDELETE 等 SQL 語句。還可以執行數據定義語言( Data Definition Language, DDL )的語句,如 CREATE TABLE 。返回受影響的行數,如果是沒有更新計數的語句,則返回 0
execute boolean execute(String sql) throws SQLException; 執行字符串中指定的 SQL 語句。可能會產生多個結果集和更新計數。如果第一個執行結果是結果集,則返回 true ;反之,返回 false 。調用 getResultSetgetUpdateCount 方法可以得到第一個執行結果
getResultSet ResultSet getResultSet() throws SQLException; 返回前一條查詢語句的結果集。如果前一條語句未產生結果集,則返回 null 。對於每一條執行過的語句,該方法只能被調用一次
getUpdateCount
getLargeUpdateCount
int getUpdateCount() throws SQLException;
default long getLargeUpdateCount() throws SQLException
返回受前一條更新語句影響的行數 如果前一條語句未更新數據庫,則返回 -1 。對於每一條執行過的語句,該方法只能被調用一次
close void close() throws SQLException; 關閉 Statement 對象以及它所對應的結果集
isClosed boolean isClosed() throws SQLException; 如果語句被關閉, 返回 true
closeOnCompletion public void closeOnCompletion() throws SQLException; 使得一旦該語句的所有結果集都被關閉,關閉該語句
java.sql.ResultSet 方法名稱 方法聲明 描述
next boolean next() throws SQLException; 將結果集中的當前行向前移動一行。如果已經到達最後一行的後面, 返回 false 。注意,初始情況下必須調用該方法才能轉到第一行
getXxx
getObjec
updateObject
Xxx getXxx(int columnNumber)
Xxx getXxx(String columnLabel)
public <T> T getObject(int columnIndex, Class<T> type) throws SQLException;
public <T> T getObject(String columnLabel, Class<T> type) throws SQLException;
default void updateObject(int columnIndex, Object x, SQLType targetSqlType) throws SQLException
default void updateObject(String columnLabel, Object x, SQLType targetSqlType) throws SQLException
Xxx 指數據類型,例如 intdoubleStringDate 。用給定的列序號或列標籤返回或更新該列的值,並將值轉換成指定的類型。列標籤是 SQL 的 AS 子句中指定的標籤,在沒有使用 AS 時,它就是列名
findColumn int findColumn(String columnLabel) throws SQLException; 根據給定的列名,返回該列的序號
close void close() throws SQLException; 立即關閉當前的結果集
isClosed boolean isClosed() throws SQLException; 如果該語句被關閉, 返回 true

管理連接、語句和結果集

每個 Connection 對象都可以創建一個或多個 Statement 對象。同一個 Statement 對象可以用於多個不相關的命令和查詢。但是,一個 Statement 對象最多隻能有一個打開的結果集。如果需要執行多個查詢操作,且需要同時分析查詢結果,那麼必須創建多個 Statement 對象

需要說明的是,至少有一種常用的數據庫( Microsoft SQL Server )的 JDBC 驅動程序只允許同時存在一個活動的 Statement 對象。使用 DatabaseMetaData 接口中的 getMaxStatements 方法可以獲取 JDBC 驅動程序支持的同時活動的語句對象的總數

實際上,我們通常並不需要同時處理多個結果集。如果結果集相互關聯,我們可以使用組合查詢,這樣就只需要分析一個結果。對數據庫進行組合查詢比使用 Java 程序遍歷多個結果集要高效得多

使用完 ResultSetStatementConnection 對象後,應立即調用 close 方法。這些對象都使用了規模較大的數據結構,它們會佔用數據庫存服務器上的有限資源

如果 Statement 對象上有一個打開的結果集,那麼調用 close 方法將自動關閉該結果集。同樣地,調用 Connection 類的 close 方法將關閉該連接上的所有語句。

反過來的情況是,在使用 Java 7 時,可以 Statement 上調用 closeOnCompletion 方法,在其所有結果集都被關閉後,該語句會立即被自動關閉

如果所用連接都是短時的,那麼無需考慮關閉語句和結果集。只需將 close 語句放在帶資源的 try 語句中,以便確保最終連接對象不可能繼續保持打開狀態

分析 SQL 異常( SQLException

每個 SQLException 都有一個由多個 SQLException 對象構成的鏈,這些對象可以通過 getNextException 方法獲取。這個異常鏈是每個異常都具有的由 Throwable 對象構成的“成因”鏈之外的異常鏈,因此,我們要用兩個嵌套的循環來完整枚舉所有的異常。幸運的是, Java 6 改進了 SQLException ,讓其實現了 Iterable<Throwable> 接口,其 iterator() 方法可以產生一個 Iterator<Throwable> ,這個迭代器可以迭代這兩個鏈,首先迭代第一個 SQLException 的成因鏈,然後迭代下一個 SQLException ,以此類推

for (Throwable t : sqlException) {
    // do something
}

可以在 SQLException 上調用 getSQLStategetErrorCode 方法來進一步分析它,其中第一個方法將產生符合 X/Open 或 SQL:2003 標準的字符串(調用 DatabaseMetaData 接口的 getSQLStateType 方法可以查出驅動程序所使用的標準)。 而錯誤代碼是與具體的提供商相關的

SQL 異常按照層次結構樹的方式組織到了一起,這使得我們可以按照與提供商無關的方式來捕獲具體的錯誤類型

img

另外,數據庫驅動程序可以將非致命問題作爲警告報告,我們可以從連接、語句和結果中獲取這些警告。SQLWarning 類是 SQLException 的子類(儘管 SQLWarning 不會被當作異常拋出),可以調用 getSQLStategetErrorCode 來獲取有關警告的更多信息

與 SQL 異常類似,警告也是串成鏈的 要獲得所有的警告,可以使用下面的循環:

SQLWarning w = stat.getWarning();
while (w != null) {
    // do something with w
    w = w.getNextWarning();
}

當數據從數據庫中讀出並意外被截斷時, SQLWarningDataTruncation 子類就派上用場了。如果數據截斷髮生在更新語句中,那麼 DataTruncation 將會被當作異常拋出

java.sql.SQLException 方法名稱 方法聲明 描述
getNextException public SQLException getNextException() 返回鏈接到該 SQL 異常的下一個 SQL 異常,或者在到達鏈尾時返回 null
iterator public Iterator<Throwable> iterator() 獲取迭代器,可以迭代鏈接的 SQL 異常和它們的成因
getSQLState public String getSQLState() 獲取 “SQL 狀態”,即標準化的錯誤代碼
getErrorCode public int getErrorCode() 獲取提供商相關的錯誤代碼
java.sql.SQLWarning 方法名稱 方法聲明 描述
getNextWarning public SQLWarning getNextWarning() 返回鏈接到該警告的下一個警告,或者在到達鏈尾時返回 null
java.sql.Connection 方法名稱 方法聲明 描述
getWarnings SQLWarning getWarnings() throws SQLException; 返回未處理警告中的第一個,或者在沒有未處理警告時返回 null

java.sql.Statementjava.sql.ResultSet 具有相同的 getWarnings 方法

java.sql.DataTruncation 方法名稱 方法聲明 描述
getParameter public boolean getParameter() 如果在參數上進行了數據截斷,則返回 true ;如果在列上進行了數據截斷,則返回 false
getIndex public int getIndex() 返回被截斷的參數或列的索引
getDataSize public int getDataSize() 返回應該被傳輸的字節數量,或者在該值未知的情況下返回 -1
getTransferSize public int getTransferSize() 返回應該被傳輸的字節數量,或者在該值未知的情況下返回 -1

執行查詢操作

預備語句( PreparedStatement

預備語句( prepared statement )

準備一個帶有宿主變量的查詢語句,每次查詢時只需爲該變量填入不同的字符串就可以反覆多次使用該語句。這一技術改進了查詢性能,每當數據庫執行一個查詢時,它總是首先通過計算來確定查詢策略,以便高效地執行查詢操作。通過事先準備好查詢並多次重用它,我們就可以確保查詢所需的準備步驟只被執行一次

在預備查詢語句中,每個宿主變量都用 ? 來表示。如果存在一個以上的變 ,那麼在設置變量值時必須注意 ? 的位置

在執行預備語句之前,必須使用 set 方法將變量綁定到實際的值上。和 ResultSet 接口中的 get 方法類似 ,針對不同的數據類型也有不同的 set 方法,位置 1 表示第一個 ?

stat.setDouble(1, priceChange);

如果想要重用已經執行過的預備查詢語句,那麼除非使用 set 方法或調用 clearParameters 方法,否則所有宿主變量的綁定都不會改變。這就意味着,在從一個查詢到另一個查詢的過程中,只需使用 setXxx 方法重新綁定那些需要改變的變量即可

提示:通過連接字符串來手動構建查詢顯得非常枯燥乏味,而且存在潛在的危險。你必須注意像引號這樣的特殊字符,而且如果查詢中涉及用戶的輸入,那就還需要警惕注入攻擊。因此,只有查詢涉及變量時,才應該使用預備語句

注意:在相關的 Connection 對象關閉之後, PreparedStatement 對象也就變得無效了。不過,許多數據庫通常都會自動緩存預備語句。如果相同的查詢被預備兩次,數據庫通常會直接重用查詢策略。因此,無需過多考慮調用 prepareStatement 的開銷

提示:許多程序員都不喜歡使用如此複雜的 SQL 語句。比較常見的方法是使用大量的 Java 代碼來迭代多個結果集,但是這種方法效率非常低。通常,使用數據庫的查詢代碼要比使用 Java 程序好得多一一這是數據庫的一個重要優點。一般而言,可以使用 SQL 解決的問題,就不要使用 Java 程序

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Scanner;


public class QueryTest {
    private static final String allQuery = "SELECT Books.Price, Books.Title FROM Books";

    private static final String authorPublisherQuery = "SELECT Books.Price, Books.Title"
            + " FROM Books, BooksAuthors, Authors, Publishers"
            + " WHERE Authors.Author_Id = BooksAuthors.Author_Id AND BooksAuthors.ISBN = Books.ISBN"
            + " AND Books.Publisher_Id = Publishers.Publisher_Id AND Authors.Name = ?"
            + " AND Publishers.Name = ?";

    private static final String authorQuery
            = "SELECT Books.Price, Books.Title FROM Books, BooksAuthors, Authors"
            + " WHERE Authors.Author_Id = BooksAuthors.Author_Id AND BooksAuthors.ISBN = Books.ISBN"
            + " AND Authors.Name = ?";

    private static final String publisherQuery
            = "SELECT Books.Price, Books.Title FROM Books, Publishers"
            + " WHERE Books.Publisher_Id = Publishers.Publisher_Id AND Publishers.Name = ?";


    private static final String priceUpdate = "UPDATE Books " + "SET Price = Price + ? "
            + " WHERE Books.Publisher_Id = (SELECT Publisher_Id FROM Publishers WHERE Name = ?)";

    private static Scanner in;
    private static ArrayList<String> authors = new ArrayList<>();
    private static ArrayList<String> publishers = new ArrayList<>();

    public static void main(String[] args) throws IOException {
        try (Connection conn = getConnection()) {
            in = new Scanner(System.in);
            authors.add("Any");
            publishers.add("Any");
            try (Statement stat = conn.createStatement()) {
                // Fill the authors array list
                String query = "SELECT Name FROM Authors";
                try (ResultSet rs = stat.executeQuery(query)) {
                    while (rs.next()) {
                        authors.add(rs.getString(1));
                    }
                }

                // Fill the publishers array list
                query = "SELECT Name FROM Publishers";
                try (ResultSet rs = stat.executeQuery(query)) {
                    while (rs.next()) {
                        publishers.add(rs.getString(1));
                    }
                }
            }
            boolean done = false;
            while (!done) {
                System.out.print("Q)uery C)hange prices E)xit: ");
                String input = in.next().toUpperCase();
                if (input.equals("Q")) {
                    executeQuery(conn);
                } else if (input.equals("C")) {
                    changePrices(conn);
                } else {
                    done = true;
                }
            }
        } catch (SQLException e) {
            for (Throwable t : e) {
                System.out.println(t.getMessage());
            }
        }
    }

    private static void executeQuery(Connection conn) throws SQLException {
        String author = select("Authors:", authors);
        String publisher = select("Publishers:", publishers);
        PreparedStatement stat;
        if (!author.equals("Any") && !publisher.equals("Any")) {
            stat = conn.prepareStatement(authorPublisherQuery);
            stat.setString(1, author);
            stat.setString(2, publisher);
        } else if (!author.equals("Any") && publisher.equals("Any")) {
            stat = conn.prepareStatement(authorQuery);
            stat.setString(1, author);
        } else if (author.equals("Any") && !publisher.equals("Any")) {
            stat = conn.prepareStatement(publisherQuery);
            stat.setString(1, publisher);
        } else {
            stat = conn.prepareStatement(allQuery);
        }

        try (ResultSet rs = stat.executeQuery()) {
            while (rs.next()) {
                System.out.println(rs.getString(1) + ", " + rs.getString(2));
            }
        }
    }


    public static void changePrices(Connection conn) throws SQLException {
        String publisher = select("Publishers:", publishers.subList(1, publishers.size()));
        System.out.print("Change prices by: ");
        double priceChange = in.nextDouble();
        PreparedStatement stat = conn.prepareStatement(priceUpdate);
        stat.setDouble(1, priceChange);
        stat.setString(2, publisher);
        int r = stat.executeUpdate();
        System.out.println(r + " records updated.");
    }


    public static String select(String prompt, List<String> options) {
        while (true) {
            System.out.println(prompt);
            for (int i = 0; i < options.size(); i++) {
                System.out.printf("%2d) %s%n", i + 1, options.get(i));
            }
            int sel = in.nextInt();
            if (sel > 0 && sel <= options.size()) {
                return options.get(sel - 1);
            }
        }
    }


    public static Connection getConnection() throws SQLException, IOException {
        Properties props = new Properties();
        try (InputStream in = Files.newInputStream(Paths.get("database.properties"))) {
            props.load(in);
        }

        String drivers = props.getProperty("jdbc.drivers");
        if (drivers != null) {
            System.setProperty("jdbc.drivers", drivers);
        }
        String url = props.getProperty("jdbc.url");
        String username = props.getProperty("jdbc.username");
        String password = props.getProperty("jdbc.password");

        return DriverManager.getConnection(url, username, password);
    }
}

讀寫 LOB

在 SQL 中,二進制大對象稱爲 BLOB ,字符型大對象稱爲 CLOB

要讀 LOB ,需要執行 SELECT 語句,然後在 ResultSet 上調 getBlobgetClob 方法,這樣就可以獲得 BlobClob 類型的對象。要從 Blob 中獲取二進制數據,可以調用 getBytesgetBinaryStream 。如果獲取了 Clob 對象,那麼就可以通過調用 getSubStringgetCharacterStream 方法來獲取其中的字符數據

要將 LOB 置於數據庫中,需要在 Connection 對象上調用 createBlobcreateClob ,然後獲取一個用於該 LOB 的輸出流或寫出器,寫出數據,並將該對象存儲到數據庫中

Blob coverBlob = connection.createBlob();
int offset = 0;
OutputStream out = coverBlob.setBinaryStream(offset);
ImageIO.write(coverImage, "PNG", out);
PreparedStatement stat = connection.prepareStatement("INSERT INTO Cover VALUES (?, ?)");
stat.setInt(1, isbn);
stat.setBlob(2, coverBlob);
java.sql.ResultSet 方法名稱 方法聲明 描述
getBlob
getClob
Blob getBlob(int columnIndex) throws SQLException;
Blob getBlob(String columnLabel) throws SQLException;
Clob getClob(int columnIndex) throws SQLException;
Clob getClob(String columnLabel) throws SQLException;
獲取給定列的 BLOBCLOB
java.sql.Blob 方法名稱 方法聲明 描述
length long length() throws SQLException; 獲取該 BLOB 的長度
getBytes byte[] getBytes(long pos, int length) throws SQLException; 獲取該 BLOB 中給定範圍的數據
getBinaryStream InputStream getBinaryStream () throws SQLException;
InputStream getBinaryStream(long pos, long length) throws SQLException;
返回一個輸入流,用於讀取該 BLOB 中全部或給定範圍的數據
setBinaryStream OutputStream setBinaryStream(long pos) throws SQLException; 返回一個輸出流,用於從給定位置開始寫入該 BLOB
java.sql.Clob 方法名稱 方法聲明 描述
length long length() throws SQLException; 獲取該 CLOB 中的字符總數
getSubString String getSubString(long pos, int length) throws SQLException; 獲取該 CLOB 中給定範圍的字符
getCharacterStream Reader getCharacterStream() throws SQLException;
Reader getCharacterStream(long pos, long length) throws SQLException;
返回一個讀入器(而不是流),用於讀取 CLOB 中全部或給定範圍的數據
setCharacterStream Writer setCharacterStream(long pos) throws SQLException; 返回一個寫出器(而不是流),用於從給定位置開始寫入該 CLOB
java.sql.Connection 方法名稱 方法聲明 描述
createBlob
createClob
Blob createBlob() throws SQLException;
Clob createClob() throws SQLException;
創建一個空的 BLOBCLOB

SQL 轉義

“轉義”語法是各種數據庫普遍支持的特性,但是數據庫使用的是與數據庫相關的語法變體,因此,將轉義語法轉譯爲特定數據庫的語法是 JDBC 驅動程序的任務之一

轉義主要用於下列場景:

  • 日期和時間字面常量
  • 調用標量函數
  • 調用存儲過程
  • 外連接
  • LIKE 子句中的轉義字符

日期和時間字面常量隨數據庫的不同而變化很大。要嵌入日期或時間字面常量,需要按照 ISO 8601 格式 指定它的值,之後驅動程序會將其轉譯爲本地格式。應該使用 dtts 來表示 DATETIMETIMESTAMP 值:

{d '2008-01-24'} 
{t '23:59:59’}
{ts '2008-01-24 23:59:59.999'}

標量函數( scalar function ) 是指僅返回單個值的函數。在數據庫中包含大量的函數,但是不同的數據庫中這些函數名存在着差異。JDBC 規範提供了標準的名字,並將其轉譯爲數據庫相關的名字。要調用函數,需要像下面這樣嵌入標準的函數名和參數:

{fn left(?, 20)} 
{fn user()}

在 JDBC 規範中可以找到它支持的函數名的完整列表

存儲過程(stored procedure ) 是在數據庫中執行的用數據庫相關的語言編寫的過程。要調用存儲過程,需要使用 call 轉義命令,在存儲過程沒有任何參數時,可以不用加上括號外,應該用 = 來捕獲存儲過程的返回值:

{call PROC1(?, ?)} 
{call PROC2} 
{call ?= PROC3(?)}

兩個表的 外連接( outer join ) 並不要求每個表的所有行都要根據連接條件進行匹配。LEFT OUTER JOINRIGHT OUTER JOINFULL OUTER JOIN ,由於並非所有的數據庫對於這些連接都使用標準的寫法,因此需要使用轉義語法

_% 字符在 LIKE 子句中具有特殊含義,用來匹配一個字符或一個字符序列。目前並不存在任何在宇面上使用它們的標準方式,所以如果想要匹配所有包含 _ 字符的字符串,就必須使用下面的結構:

... WHERE ? LIKE %!_% {escape '!'}

這裏我們將 ! 定義爲轉義字符,而 !_ 組合表示字面常量下劃線

多結果集

在執行存儲過程,或者在使用允許在單個查詢中提交多個 SELECT 語句的數據庫時,一個查詢有可能會返回多個結果集。下面是獲取所有結果集的步驟:

  1. 使用 execute 方法來執行 SQL 語句
  2. 獲取第一個結果集或更新計數
  3. 重複調用 getMoreResults 方法以移動到下一個結果集
  4. 當不存在更多的結果集或更新計數時,完成操作

如果由多結果集構成的鏈中的下一項是結果集, executegetMoreResults 方法將返回 true ,而如果在鏈中的下一項不是更新計數, getUpdateCount 方法將返回 -1

boolean isResult = stat.execute(command);
boolean done = false;
while (!done) {
    if (isResult) {
        ResultSet resultSet = stat.getResultSet();
        // do something
    } else {
        int updateCount = stat.getUpdateCount();
        if (updateCount >= 0) {
            // do something
        } else {
            done = true;
        }
    }
    if (!done) {
        isResult = stat.getMoreResults();
    }
}
java.sql.Statement 方法名稱 方法聲明 描述
getMoreResults boolean getMoreResults() throws SQLException;
boolean getMoreResults(int current) throws SQLException;
獲取該語句的下一個結果集, current 參數是 Statement.CLOSE_CURRENT_RESULT (默認值),KEEP_CURRENT_RESULTCLOSE_ALL_RESULTS 之一。如果存在下一個結果集,並且它確實是一個結果集 ,則返回 true

獲取自動生成的鍵

大多數數據庫都支持某種在數據庫中對行自動編號的機制。但是,不同的提供商所提供的機制之間存在着很大的差異, 這些自動編號的值經常用作主鍵。儘管 JDBC 沒有提供獨立於提供商的自動生成鍵的解決方案,但是它提供了獲取自動生成鍵的有效途徑。當我們向數據表中插入一個新行,且其鍵自動生成時 ,可以用下面的代碼來獲取這個鍵

stat.executeUpdate(insertStatment, Statement.RETURN_GENERATED_KEYS);
ResultSet rs = stat.getGeneratedKeys();
if (rs.next()) {
    int key = rs.getInt(1);
    // do something
}
java.sql.Statement 方法名稱 方法聲明 描述
execute
executeUpdate
boolean execute(String sql, int autoGeneratedKeys) throws SQLException;
int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException;
像前面描述的那 行給定 SQL 語句,如果 autoGeneratedKeys 被設置爲 Statement.RETURN GENERATED_KEYS ,並且該語句是一條 INSERT 語句, 那麼第一列中就是自動生成的鍵

可滾動和可更新的結果集

使用 ResultSet 接口中 next 方法可以迭代遍歷結果集中的所有行

對於可滾動結果集而言,可以在其中向前或向後移動,甚至可以跳到任意位置

在可更新的結果集中,可以以編程方式來更新其中的項,使得數據庫可以自動更新數據

可滾動的結果集

默認情況下,結果集是不可滾動和不可更新的 。爲了從查詢中獲取可滾動的結果集,必須使用下面的方法得到一個不同的 Statement 對象:

Statement stat = conn.createStatement(resultSetType, resultSetConcurrency);
PreparedStatement preparedStatement = conn.prepareStatement(sql, resultSetType, resultSetConcurrency);

resultSetTyperesultSetConcurrency 取值來自 ResultSet 定義的常量

resultSetType 描述
TYPE_FORWARD_ONLY 結果集不能滾動(默認值)
TYPE_SCROLL_INSENSITIVE 結果集可以滾動,但對數據庫變化不敏感
TYPE_SCROLL_SENSITIVE 結果集可以滾動,且對數據庫變化敏感
resultSetConcurrency 描述
CONCUR_READ_ONLY 結果集不能用於更新數據庫 (默認值)
CONCUR_UPDATABLE 結果集可以用於更新數據庫

如果只想滾動遍歷結果集,而不想編輯它的數據,那麼可以使用以下語句:

Statement stat = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
ResultSet rs = stat.executeQuery(query);

現在,獲得的所有結果集都將是可滾動的。可滾動的結果集有一個遊標,用以指示當前位置。

注意: 並非所有的數據庫驅動程序都支持可滾動和可更新的結果集。(使用 DatabaseMetaData 接口中 supportsResultSetTypesupportsResultSetConcurrency 方法,我們可以獲知在使用特定的驅動程序時,某個數據庫究竟支持哪些結采集類型以及哪些併發模式 )。即使是數據庫支持所有的結果集模式,某個特定的查詢也可能無法產生帶有所請求的所有屬性的結果集。(例如,一個複雜查詢的結果集就有可能是不可更新的結果集 )在這種情況下,executeQuery 方法將返回一個功能較少的 ResultSet 對象,並添加 SQLWarning 到連接對象中。或者,也可以使用 ResultSet 中的 getTypegetConcurrency 方法查看結果集實際支持的模式。如果不檢查結果集的功能就發起一個不支持的操作, 如對不可滾動的結果集調用 previous 方法,那麼程序將拋出一個 SQLException 異常

DatabaseMetaData metaData = connection.getMetaData();

boolean b1 = metaData.supportsResultSetType(ResultSet.TYPE_FORWARD_ONLY);
boolean b2 = metaData.supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE);
boolean b3 = metaData.supportsResultSetType(ResultSet.TYPE_SCROLL_SENSITIVE);
System.out.println("supportsResultSetType :: " + b1 + " , " + b2 + " , " + b3);

boolean b11 = metaData.supportsResultSetConcurrency(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
boolean b12 = metaData.supportsResultSetConcurrency(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);
boolean b21 =
        metaData.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
boolean b22 =
        metaData.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
boolean b31 =
        metaData.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
boolean b32 =
        metaData.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
System.out.println(
        "supportsResultSetConcurrency :: " + b11 + " , " + b12 + " , " + b21 + " , " + b22 + " , " + b31 + " , "
                + b32);

結果集向後滾動:

if (rs.previous()) {
    // do something
}

如果遊標位於一個實際的行上,那麼該方法將返回 true ;如果遊標位於第一行之前,那麼返回 false

將遊標向後或向前移動多行:

rs.relative(n);

如果 n 爲正數,遊標將向前移動。如果 n 爲負數,遊標將向後移動。如果 n 爲 0 ,那麼調用該方法將不起任何作用。如果試圖將遊標移動到當前行集的範圍之外, 根據 n 值的正負號,遊標需要被設置在最後一行之後或第一行之前,那麼,該方法將返回 false ,且不移動遊標。如果遊標位於一個實際的行上,那麼該方法將返回 true

將遊標設置到指定的行號上:

rs.absolute(n);

返回當前行的行號:

int currentRow = rs.getRow();

結果集中第一行的行號爲 1 。如果返回值爲 0 ,那麼當前遊標不在任何行上,它要麼位於第一行之前,要麼位於最後一行之後

firstlastbeforeFirstafterLast 這些簡便方法用於將遊標移動到第一行、最後一行、第一行之前或最後一行之後

isFirstisLastisBeforeFirstisAfterLast 用於測試遊標是否位於這些特殊位置上

可更新的結果集

如果希望編輯結果集中的數據,並且將結果集上的數據變更自動反映到數據庫中,那麼就必須使用可更新的結果集 可更新的結果集並非必須是可滾動的,但如果將數據提供給用戶去編輯,那麼通常也會希望結果集是可滾動的

獲得可更新的結果集:

Statement stat = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
ResultSet rs = stat.executeQuery(sql);

並非所有的查詢都會返回可更新的結果集。 如果查詢涉及多個表的連接操作,那麼它所產生的結果集將是不可更新的。如果查詢只涉及一個表,或者在查詢時是使用主鍵連接多個表的,那麼它所產生的結果集將是可更新的結果集。可以調用 ResultSet 接口中的 getConcurrency 方法來確定結果集是否是可更新的

假設想提高某些圖書的價格,但是在執行 UPDATE 語句時又沒有一個簡單而統一的提價標準。此時,就可以根據任意設定的條件,迭代遍歷所有的圖書並更新它們的價格

String query = "SELECT * FROM Books";
ResultSet rs = stat.executeQuery(query);
while (rs.next()) {
    if (...){
        double increase = ...
        double price = rs.getDouble("Price");
        rs.updateDouble("Price", price + increase);
        rs.updateRow(); // make su to call updateRow afte updating fie 1 ds
    }
}

所有對應於 SQL 類型的數據類型都配有 updateXxx 方法,比如 updateDoubleupdateString 等。與 getXxx 方法相同,在使用 updateXxx 方法時必須指定列的名稱或序號。然後,你可以給該字段設置新的值

在使用第一個參數爲列序號的 updateXxx 方法時,請注意這裏的列序號指的是該列在結果集中的序號。它的值可以與數據庫中的列序號不同

updateXxx 方法改變的只是結果集中的行值,而非數據庫中的值。當更新完行中的字段值後,必須調用 updateRow 方法,這個方法將當前行中的所有更新信息發送給數據庫。如果沒有調用 updateRow 方法就將遊標移動到其他行上,那麼對此行所做的所有更新都將被丟棄,而且永遠也不會被傳遞給數據庫。還可以調用 cancelRowUpdates 方法來取消對當前行的更新

如果想在數據庫中添加一條新的記錄,首先需要使用 moveToInsertRow 方法將遊標移動到特定的位置,我們稱之爲 插入行( insert row )。然後,調用 updateXxx 方法在插入行的位置上創建一個新的行。在上述操作全部完成之後,還需要調用 insertRow 方法將新建的行發送給數據庫。完成插入操作後,再調用 moveToCurrentRow 方法將遊標移回到調用 moveToInsertRow 方法之前的位置

rs.moveToInsertRow();
rs.updateString("Title", title);
rs.updateString("ISBN", isbn);
rs.updateString("Publisher_Id", pubid);
rs.updateDouble("Price", price);
rs.insertRow();
rs.moveToCurrentRow();

請注意,你無法控制在結果集或數據庫中添加新數據的位置

對於在插入行中沒有指定值的列,將被設置爲 SQL 的 NULL 。但是,如果這個列有 NOT NULL 約束 ,那麼將會拋出異常,而這一行也無法插入

刪除遊標所指的行:

rs.deleteRow();

deleteRow 方法會立即將該行從結果集和數據庫中刪除

ResultSet 接口中的 updateRowinsertRowdeleteRow 方法的執行效果等同於 SQL 命令中的 UPDATEINSERTDELETE

對大多數程序性的修改而言,使用 SQL 的 UPDATE 語句更合適一些

java.sql.Connection 方法名稱 方法聲明 描述
createStatement
prepareStatement
Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException;
PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException;
創建一個語句或預備語句,且該語句可以產生指定類型和併發模式的結果集
java.sql.ResultSet 方法名稱 方法聲明 描述
getType int getType() throws SQLException; 返回結果集的類型
getConcurrency int getConcurrency() throws SQLException; 返回結果集的併發設置
previous boolean previous() throws SQLException; 將遊標移動到前一行。如果遊標位於某一行上, 返回 true ;如果遊標位於第一行之前的位置, 返回 false
getRow int getRow() throws SQLException; 得到當前行的序號 所有行從 1 開始編號
absolute boolean absolute( int row ) throws SQLException; 移動遊標到第 row 行。如果遊標位於某一行上, 返回 true
relative boolean relative( int rows ) throws SQLException; 將遊標移動 row 行。如果 row 爲負數,則遊標向後移動。如果遊標位於某一行上, 返回 true
first
last
boolean first() throws SQLException;
boolean last() throws SQLException;
移動遊標到第一行或最後一行。如果遊標位於某一行上, 返回 true
beforeFirst
afterLast
void beforeFirst() throws SQLException;
void afterLast() throws SQLException;
移動遊標到第一行之前或最後一行之後的位置
isFirst
isLast
boolean isFirst() throws SQLException;
boolean isLast() throws SQLException;
測試遊標是否在第一行或最後一行
isBeforeFirst
isAfterLast
boolean isBeforeFirst() throws SQLException;
boolean isAfterLast() throws SQLException;
測試遊標是否在第一行之前或最後一行之後的位置
moveToInsertRow void moveToInsertRow() throws SQLException; 移動遊標到插入行。插入行是一個特殊的行,可以在該行上使用 updateXxxinsertRow 方法來插入新數據
moveToCurrentRow void moveToCurrentRow() throws SQLException; 將遊標從插入行移回到調用 moveToInsertRow 方法之前它所在的那一行
insertRow void insertRow() throws SQLException; 將插入行上的內容插入到數據庫和結果集中
deleteRow void deleteRow() throws SQLException; 從數據庫和結果集中刪除當前行
updateXxx void updateXxx(int column, Xxx data)
void updateXxx(String columnName, Xxx data)
(Xxx 指數據類型,比如 intdoubleStringDate 等)更新結果中當前行上的某個字段值
updateRow void updateRow() throws SQLException; 將當前行的更新信息發送到數據庫
cancelRowUpdates void cancelRowUpdates() throws SQLException; 撤銷對當前行的更新
java.sql.DatabaseMetaData 方法名稱 方法聲明 描述
supportsResultSetType boolean supportsResultSetType(int type) throws SQLException; 如果數據庫支持給定類型的結果集, 返回 true
supportsResultSetConcurrency boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException; 如果數據庫支持給定類型和併發模式的結果集, 返回 true

行集( Rowset

可滾動的結果集雖然功能強大,卻有一個重要的缺陷:在與用戶的整個交互過程中,必須始終與數據庫保持連接。這種方式存在很大的問題,因爲數據庫連接屬於稀有資源。在這種情況下,我們可以使用行集 Rowset 接口擴展自 ResultSet 接口,卻無需始終保持與數據庫的連接

行集還適用於將查詢結果移動到複雜應用的其他層,或者是諸如手機之類的其他設備中。你可能從未考慮過移動一個結果集,因爲它的數據結構非常龐大,且依賴於數據連接

構建行集

javax.sql.rowset 提供的接口,它們都擴展了 RowSet 接口:

  • CachedRowSet 允許在斷開連接的狀態下執行相關操作
  • WebRowSet 對象代表了一個被緩存的行集,該行集可以保存爲 XML 文件。該文件可以移動到 Web 應用的其他層中,只要在該層中使用另一個 WebRowSet 對象重新打開該文件即可
  • FilteredRowSetJoinRowSet 接口支持對行集的輕量級操作,它們等同於 SQL 中的 SELECTJOIN 操作。這兩個接口的操作對象是存儲在行集中的數據,因此運行時無需建立數據庫連接
  • JdbcRowSetResultSet 接口的一個瘦包裝器。它在 RowSet 接口中添加了有用的方法

一種獲取行集的標準方式:

RowSetFactory factory = RowSetProvider.newFactory();
CachedRowSet crs = factory.createCachedRowSet();

獲取其他行集類型的對象也有類似的方法

另外,JDK 在 com.sun.rowset 中還提供了參考實現 ,這些實現類的名字以 Impl 結尾,例如 CachedRowSetImpl 如果你無法使用 RowSetProvider ,那麼可以使用下面的類取而代之:

CachedRowSet crs = new com.sun.rowset.CachedRowSetImpl();

被緩存的行集 ( CachedRowSet )

一個被緩存的行集中包含了一個結果集中所有的數據。CachedRowSetResultSet 接口的子接口,所以你完全可以像使用結果集一樣來使用被緩存的行集。被緩存的行集有一個非常重要的優點:斷開數據庫連接後仍然可以使用行集。在執行每個用戶命令時,我們只需打開數據庫連接、執行查詢操作、將查詢結果放入被緩存的行集,然後關閉數據庫連接即可。

我們甚至可以修改被緩存的行集的數據。當然,這些修改不會立刻反饋到數據庫中。相反,必鬚髮起一個顯式的請求,以便讓數據庫真正接受所有修改。CachedRowSet 會重新連接到數據庫,並通過執行 SQL 語句向數據庫中寫入所有修改後的數據

javax.sql.RowSet 方法名稱 方法聲明 描述
getUrl
setUrl
String getUrl() throws SQLException;
void setUrl(String url) throws SQLException;
獲取或設置數據庫的 URL
getUsername
setUsername
String getUsername();
void setUsername(String name) throws SQLException;
獲取或設置連接數據庫所需的用戶名
getPassword
setPassword
String getPassword();
void setPassword(String password) throws SQLException;
獲取或設置連接數據庫所需的密碼
getCommand
setCommand
String getCommand();
void setCommand(String cmd) throws SQLException;
獲取或設置向行集中填充數據時需要執行的命令
execute void execute() throws SQLException; 通過執行使用 setCommand 方法設置的語句集來填充行集。爲了使驅動管理器可以獲得連接, 須事先設定 URL 用戶名和密碼

元數據

JDBC 還可以提供關於數據庫及其表結構的詳細信息

在 SQL 中,描述數據庫或其組成部分的數據稱爲元數據(區別於那些存在數據庫中的實際數據)。

我們可以獲得三類元數據:

  • 關於數據庫的元數據
  • 關於結果集的元數據
  • 關於預備語句參數的元數據

DatabaseMetaData 接口中有上百個方法可以用於查詢數據庫的相關信息,包括一些使用奇特的名字進行調用的方法

DatabaseMetaData 接口用於提供有關數據庫的數據,第二個元數據接口 ResultSetMetaData 則用於提供結果集的相關信息。

java.sql.Connection 方法名稱 方法聲明 描述
getMetaData DatabaseMetaData getMetaData() throws SQLException; 返回 DatabaseMetaData 對象,該對象封裝了有關數據庫連接的元數據
java.sql.DatabaseMetaData 方法名稱 方法聲明 描述
getTables ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String types[]) throws SQLException; 返回某個目錄( catalog )中所有表的描述,該目錄必須匹配給定的模式( schema )、表名字模式以及類型標準 (模式用於描述一組相關的表和訪問權限,而目錄描述的是組相關的模式,這些概念對組織大型數據庫非常重要)
getJDBCMajorVersion
getJDBCMinorVersion
int getJDBCMajorVersion() throws SQLException;
int getJDBCMinorVersion() throws SQLException;
返回建立數據庫連接的 JDBC 驅動程序的主版本號和次版本號。例如, JDBC 4.2 的驅動程序有一個主版本號 4 和一個次版本號 2
getMaxConnections int getMaxConnections() throws SQLException; 返回可同時連接到數據庫的最大併發連接數
getMaxStatements getMaxStatements 返回單個數據庫連接允許同時打開的最大併發語句數 如果對允許打開的語句數目沒有限制或者不可知, 返回 0
java.sql.ResultSet 方法名稱 方法聲明 描述
getMetaData ResultSetMetaData getMetaData() throws SQLException; 返回與當前 ResultSet 對象中的列相關的元數據
java.sql.ResultSetMetaData 方法名稱 方法聲明 描述
getColumnCount int getColumnCount() throws SQLException; 返回當前 ResultSet 對象中的列數
getColumnDisplaySize int getColumnDisplaySize(int column) throws SQLException; 返回給定列序號的列的最大寬度
getColumnLabel String getColumnLabel(int column) throws SQLException; 返回該列所建議的名稱
getColumnName String getColumnName(int column) throws SQLException; 返回指定的列序號所對應的列名

事務

可以將一組語句構建成一個 事務( transaction )。當所有語句都順利執行之後,事務可以被 提交( commit )。否則,如果其中某個語句遇到錯誤,那麼事務將被回滾,就好像沒有任何語句被執行過一樣

將多個語句組合成事務的主要原因是爲了確保 數據庫完整性( database integrity )

對 JDBC 對事務編程

默認情況下,數據庫連接處於 自動提交模式( autocommit mode )。每個 SQL 語句一旦被執行便被提交給數據庫。一旦命令被提交,就無法對它進行回滾操作。在使用事務時, 需要關閉這個默認值:

conn.setAutoCommit(false);
// 創建一個語句對象:
Statement stat = conn.createStatement();

// 任意多次地調用 executeUpdate 方法:
stat.executeUpdate(command1);
stat.executeUpdate(command2);
stat.executeUpdate(command3);

// 如果執行了所有命令之後沒有出錯,則調用 commit 方法
conn.commit();

// 如果出現錯誤,則調用:
conn.rollback();

保存點

在使用某些驅動程序時,使用 保存點( save point ) 可以更細粒度地控制回滾操作。創建一個保存點意味着稍後只需返回到這個點,而非事務的開頭

Statement stat = conn.createStatement();    // start transaction; rollback() goes here
stat.executeUpdate(command1);
Savepoint svpt = conn.setSavepoint();    // set savepoint; rollback(svpt) goes here
stat.executeUpdate(command2);

if (condition) {
    conn.rollback(svpt);
}

conn.commit();

當不再需要保存點時,必須釋放它:

conn.releaseSavepoint(svpt);

批量更新

假設有一個程序需要執行許多 INSERT 語句,以便將數據填入數據庫表中,此時可以使用批量更新的方法來提高程序性能。在使用 批量更新( batch update ) 時,一個語句序列作爲一批操作將同時被收集和提交

使用 DatabaseMetaData 接口中的 supportsBatchUpdates 方法可以獲知數據庫是否支持這種特性

處於向一批中的語句可以是 INSERTUPDATEDELETE 等操作,也可以是數據庫定義語句,如 CREATE TABLEDROP TABLE 。但是,在批量處理中添加 SELECT 語句會拋出異常

boolean autoCommit = conn.getAutoCommit();
conn.setAutoCommit(false);
Statement stat = conn.createStatement();

stat.addBatch(command);

while (condition) {
    // do something
    stat.addBatch(command);
}

// 爲所有已提交的語句返回一個記錄數的數組
int[] counts = stat.executeBatch();

conn.commit();
conn.setAutoCommit(autoCommit);
java.sql.Connection 方法名稱 方法聲明 描述
getAutoCommit
setAutoCommit
boolean getAutoCommit() throws SQLException;
void setAutoCommit(boolean autoCommit) throws SQLException;
獲取該連接中的自動提交模式,或將其設置爲 b 。如果自動更新爲 true ,那麼所有語句將在執行結束後立刻被提交
commit void commit() throws SQLException; 提交自上次提交以來所有執行過的語句
rollback void rollback() throws SQLException; 撤銷自上次提交以來所有執行過的語句所產生的影響
setSavepoint Savepoint setSavepoint() throws SQLException;
Savepoint setSavepoint(String name) throws SQLException;
設置一個匿名或具名的保存點
rollback void rollback(Savepoint savepoint) throws SQLException; 回滾到給定保存點
releaseSavepoint void releaseSavepoint(Savepoint savepoint) throws SQLException; 釋放給定的保存點
java.sql.Savepoint 方法名稱 方法聲明 描述
getSavepointId int getSavepointId() throws SQLException; 獲取該匿名保存點的 ID 。如果該保存點具有名字, 拋出一個 SQLException 異常
getSavepointName String getSavepointName() throws SQLException; 獲取該保存點的名稱。如果該對象爲匿名保存點,則拋出一個 SQLException 異常
java.sql.Statement 方法名稱 方法聲明 描述
addBatch void addBatch( String sql ) throws SQLException; 添加命令到該語句當前的批量命令中
executeBatch
executeLargeBatch
int[] executeBatch() throws SQLException;
default long[] executeLargeBatch() throws SQLException
執行當前批量更新中的所有命令。返回一個記錄數的數組,其中每一個元素都對應一條語句,如果其值非負, 表示受該語句影響的記錄總數;如果其值爲 SUCCESS_NO_INFO 。表示該語句成功執行了,但沒有記錄數可用;如果其值爲 EXECUTE_FAILED ,則表示該語句執行失敗了
java.sql.DatabaseMetaData 方法名稱 方法聲明 描述
supportsBatchUpdates boolean supportsBatchUpdates() throws SQLException; 如果驅動程序支持批量更新,則返回 true

高級 SQL 類型

JDBC 支持的 SQL 數據類型以及它們在 Java 語言中對應的數據類型:

SQL 數據類型 Java 數據類型
INTEGERINT int
SMALLINT short
NUMERIC(m ,n) , DECIMAL(m ,n)DEC(m ,n) java.math.BigDecimal
FLOAT(n) double
REAL float
DOUBLE double
CHARACTER(n)CHAR(n) String
VARCHAR(n) , LONG VARCHAR String
BOOLEAN boolean
DATE java.sql.Date
TIME java.sql.Time
TIMESTAMP java.sql.Timestamp
BLOB java.sql.Blob
CLOB java.sql.Clob
ARRAY java.sql.Array
ROWID java.sql.RowId
NCHAR(n) , NVARCHAR(n) , LONG NVARCHAR String
NCLOB java.sql.NClob
SQLXML java.sql.SQLXML

Web 與企業應用中的連接管理

在 Web 或企業環境中部署 JDBC 應用時,數據庫連接管理與 Java 名字和目錄接口( JNDI )是集成在一起的。遍佈企業的數據源的屬性可以存儲在一個目錄中,採用這種方式使得可以集中管理用戶名、密碼、數據庫名和 JDBC URL

使用 JNDI 服務來定位數據源,創建數據庫連接:

Context jndiContext = new InitialContext();
DataSource source = (DataSource) jndiContext.lookup("java:comp/env/jdbc/corejava");
Connection conn = source.getConnection();

數據源就是一個能夠提供簡單的 JDBC 連接和更多高級服務的接口,比如執行涉及多個數據庫的分佈式事務。 javax.sql 標準擴展包定義了 DataSource 接口

在 Java EE 容器 ,甚至不必編程進行 JNDI 查找,只需在 DataSource 域上使用 Resource 註解,當加載應用時 ,這個數據源引用將被設置:

@Resource(name = "jdbc/corejava")
private DataSource source;

數據庫連接是有限的資源,如果用戶要離開應用一段時間,那麼他佔用的連接就不應該保持打開狀態;另一方面,每次查詢都獲取連接並在隨後關閉它的代價也是相當高的。

解決問題的方法是建立數據庫 連接池( pool )。這意味着數據庫連接在物理上並未被關閉,而是保留在一個隊列中並被反覆重用。連接池是一種非常重要的服務, JDBC 規範爲實現者提供了用以實現連接池服務的手段。不過,JDK 本身並未實現這項服務,數據庫供應商提供的 JDBC 驅動程序中通常也不包含這項服務。相反, Web 容器和應用服務器的開發商通常會提供連接池服務的實現

連接池的使用對程序員來說是完全透明的,可以通過獲取數據源並調用 getConnection 方法來得到連接池中的連接。使用完連接後,需要調用 close 方法。該方法並不在物理上關閉連接,而只是告訴連接池已經使用完該連接。連接池通常還會將池機制作用於預備語句上

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章