寫給求甚解的你---JDBC詳解

​ 說起JDBC,估計大家都能說出個一、二、三出來,畢竟JDBC的大名如雷灌耳,Mybaitis、Hibernate、Sharding-JDBC等,都是對JDBC進行的封裝,因此理解JDBC的理論基礎,對我們以後的開發大有裨益。

​ 下面,就讓我們一起來看下JDBC,讓自己的武器庫中多一種基礎理論。

1.JDBC簡介

​ 在開發過程中,數據庫的開發是非常重要的,用戶的個人信息、操作記錄、內容信息(如商品、新聞)等等等,幾乎所有的數據都是需要存儲在數據中的。尤其是Web系統在10世紀90年代的起飛,Java和數據庫之間發生了一些美妙的故事,Sun公司爲了可以使Java能支持數據庫的訪問,爲Java提供了一套訪問數據庫的標準的類庫------JDBC。這套類庫位於jdk提供的基礎jar包–rt.jar中,具體的位置在java.sql包中。

​ 那什麼是JDBC呢?JDBC的全稱爲Java數據庫連接(Java DataBase Connectivity),它是一套用與執行SQL語句的Java API,是Java訪問數據庫的一整套規範

資源分配圖

​ 從上圖中,可以看到,JDBC和各種數據庫之間的連接(我們也稱之爲連接驅動),連接JDBC這一端的都是統一規格的,而連接到MySQL、Oracle、PostgreSQL等數據庫的一端是各不相同的,這也是爲什麼說JDBC是Java操作數據庫的標準,因爲他(JDBC)要求各個數據庫廠商按照統一的規範來提供數據庫連接驅動,各個連接驅動中,都需要實現JDBC中的接口,而JDBC不去關心各個驅動中具體的實現細節。

​ 通過這種方式,使得在Java程序中連接數據庫,只需要對接JDBC即可,如果連接MySQL數據庫,則裝入MySQL Connector,**如果某天數據庫更換爲Oracle,只需更換驅動程序、數據庫驅動註冊(後文有解釋)、特殊的SQL函數(某數據庫專有的)**即可。這樣開發人員就可以不必直接與底層的數據庫交互,使得代碼的通用性更強,且更加不容易出錯,數據庫開發也就更容易。

2.JDBC中常用的接口

​ 工欲善其事,必先利其器。我們直接對接JDBC,那就需要我們對JDBC設計的這套遊戲規則有足夠的瞭解。上文我們也說過了,JDBC涉及的接口主要在java.sql包中,讓我們一起來了解一些重要的接口和類。

資源分配圖

2.1 Driver接口

​ Driver接口是所有數據庫連接程序必須要實現的接口。JDBC要求,當驅動程序中實現Driver接口的類被加載,它應創建其自身的實例並在DriverManager中註冊。

​ 我們以MySQL Connector爲例來進行深點的分析(可自行去MySQL官網下載),其中的Driver類如下所示:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    //
    // Register ourselves with the DriverManager
    //
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

    /**
     * Construct a new driver and register it with DriverManager
     * 
     * @throws SQLException
     *             if a database error occurs.
     */
    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

java.sql.Driver接口中的方法是在NonRegisteringDriver類中實現的,我們從上面代碼的靜態代碼塊中可以看到,當com.mysql.jdbc.Driver被加載到內存中時,會自動的創建一個自身的實例(new Driver()),並且將其註冊到java.sql.DriverManager中。

​ 在註冊完成之後,我們就可以不用去關心任何MySQL Connector中的技術細節了,但是想研究源碼的除外😝。

​ 因此,我們註冊的數據庫驅動的方式有兩種:

  1. java.sql.DriverManager.registerDriver(new Driver),其中的Driver爲數據庫驅動中的Driver接口的實現類;
  2. Class.forName("xxx.Driver"),xxx爲包路徑,如mysql-connector中路徑爲"com.mysql.jdbc.Driver"

對於上面兩種方式,我們推薦使用Class.forName("xxx.Driver")方式,實例化時讓其自動註冊;如果使用第一種方式,則會Driver會被實例化兩次(靜態代碼塊 + new)。

​ Driver接口中還提供了一些來獲取驅動信息的類,方法如下:

方法名 請求重定向
Connection connect(String url, java.util.Properties info) 嘗試建立到給定URL的數據庫連接。如果子協議錯誤,則返回null
boolean acceptsURL(String url) 檢索驅動程序是否認爲它可以打開給定URL的連接
DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info) 獲取有關此驅動程序可能的屬性的信息
int getMajorVersion() 檢索驅動程序的主版本號
int getMinorVersion() 獲取驅動程序的次要版本號
boolean jdbcCompliant() 判斷驅動程序是否能通過JDBC遵從性測試

​ 其中子協議爲url中"jdbc:mysql://localhost:3306/java_web"中的"mysql"。

2.2 DriverManager類

DriverManager類的基本服務是用來管理一組JDBC驅動(因爲所有的數據庫驅動都會註冊到DriverManager類中),並且可以用於加載JDBC驅動和創建與數據庫之間的的連接(Connection)。

DriverManager類提供了註冊驅動程序的方法,除了上面我們提到的registerDriver方法外,DriverManager類會自動的加載系統屬性(system property)中的"jdbc.drivers"中設計到的驅動類(Driver的實現類),示例代碼如下(可以同時寫多個驅動類的地址,使用’:'隔開)。我們可以通過設置JVM的啓動參數來配置默認註冊數據庫驅動。

jdbc.drivers=foo.bah.Driver:wombat.sql.Driver:bad.taste.ourDriver

​ 在DriverManager源碼中,類說明中有這麼一段話,其中的意思就是在應用中再也不用去使用Class.forName()顯示的加載JDBC的驅動了。

Applications no longer need to explicitly load JDBC drivers using <code>Class.forName()</code>. Existing programs which currently load JDBC drivers using <code>Class.forName()</code> will continue to work without modification.
資源分配圖

​ What,剛教了三種註冊JDBC驅動的方法,你告訴我以後用不到了。我們來看下DriverManager類中初始化部分,源碼如下,我們可以看到,靜態代碼塊中會調用loadInitialDrivers方法,該方法通過檢查系統屬性(“jdbc.drivers”)和ServiceLoader機制來加載JDBC的驅動程序。

public class DriverManager {
  //...
  /**
       * Load the initial JDBC drivers by checking the System property
       * jdbc.properties and then use the {@code ServiceLoader} mechanism
       */
  static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
  }
  //...
}
資源分配圖

​ 既然您(JDBC)都這麼費心了,只能說一句您厲害了。不過項目中原有的使用了Class.forName()的地方,也不需要修改了,並不會影響功能,不過還是要吐槽一句,我原來都已經習慣了啊~~~~。

​ 鑑於DriverManager類中的方法比較多,我們這裏只介紹幾個重要的方法:

方法名 功能描述
Connection getConnection(String url, String user, String password) 建立給定URL的數據庫連接,並返回表示連接的Connection對象
Driver getDriver(String url) 根據給定的url從已經註冊的驅動集合中選取一個最適當的驅動返回
void registerDriver(java.sql.Driver driver) 註冊給定的驅動程序

​ 其中的getConnection還有另兩個重載的方法,如下所示:

  1. Connection getConnection(String url):用戶名、密碼在url中,如:“jdbc:mysql://localhost:3306/java_web?user=root&password=123456”;
  2. Connection getConnection(String url, java.util.Properties info):將用戶名和密碼放入Properties中,用戶名、密碼對應的key分別爲"user"和"password";

​ 非常有意思的是,數據庫連接的建立,是由DriverManager類自動找到一個最適合的驅動(通過源碼查看,發現其實是獲取所有的數據庫驅動,循環測試是否可以建立連接),並將建立連接的工作交由JDBC驅動中的Driver類來處理。這裏可以參考2.1中的connect方法.

2.3 Connection接口

Connection接口表示與特定數據庫的連接(Session),在連接上下文中執行sql語句並返回結果。一個連接到數據庫的Connection對象的能夠提供描述庫中表的信息,它支持的SQL語法,它的存儲過程,這個連接的功等等等。這些信息是通過Connection中的getMetaData方法獲得的。

​ 這裏有個概念需要明確下,不管是後面要將的Statement還是PrePareStatement,其執行sql都是在連接的上下文中執行的,可以理解成Connection的保持是應用程序(客戶端)和數據庫服務器的一次會話。

​ 下面我們來介紹Connection接口中定義的常用的幾種方法:

方法名 功能描述
DatabaseMetaData getMetaData() 返回一個DatabaseMetaData對象,該對象包含關於數據庫的元數據。
Statement createStatement() 創建並返回一個Statement對象,用於向數據庫發送SQL語句。
PreparedStatement prepareStatement(String sql) 創建並返回一個PreparedStatement對象,用於向數據庫發送SQL語句。傳入的SQL語句會被預編譯。
CallableStatement prepareCall(String sql) 創建並返回一個CallableStatement對象,用於調用數據庫中的存儲過程。

​ 其中的Statement對象適合執行無參數的sql語句,但是如果需要執行許多次,PreparedStatement對象效率會更好,並且PreparedStatement對象支持SQL預編譯,可以用來高效的執行同一個語句多次。

2.4 Statement接口

Statement對象用與執行靜態的SQL語句,並返回產生的結果對象。Statement接口的實例化對象可通過createStatement方法獲得。

​ 需要注意的是,一個Statement對象同一時間只能返回一個ResultSet對象。因此當發生併發時,同一時刻的ResultSet對象要由不同的Statement對象生成。如果起衝突的話,當前的的ResultSet對象會被關閉。

​ 下面我們一起來看下Statement接口中定義的常用的幾種方法:

方法名 功能描述
boolean execute(String sql) 執行給定的SQL,返回執行一個boolean類型的結果
ResultSet getResultSet() 以ResultSet的形式返回當前SQL執行結果。每次SQL執行執行後調用一次,如果結果更新數量或者沒有執行結果,則返回null
boolean getMoreResults()
int getUpdateCount() 獲取當前作爲更新數量,如果結果是一個ResultSet或者沒有執行結果,則返回-1
ResultSet executeQuery(String sql) 執行給定的查詢(SELECT)SQL,返回一個ResultSet對象
int executeUpdate(String sql) 創建並返回一個PreparedStatement對象,用於向數據庫發送SQL語句。傳入的SQL語句會被預編譯。

​ 需注意getResultSetgetUpdateCount方法,需要在execute方法執行返回成功後才能獲取到值,並且一次執行只能獲取一次。因此如果不是在執行一個動態的未知SQL,儘量不要使用execute方法。

2.5 PreparedStatement接口

PreparedStatement是一個用於表示預編譯的SQL語句的對象。傳入的SQL語句已完成預編譯並存儲在PreparedStatement對象中,之後就可以可以使用該對象有效地多次執行該語句

​ 通過上面一段話,我們就可以簡單的看出StatementPreparedStatement的區別,即出於提高多次執行同一個SQL的效率,PreparedStatement對象會先對SQL進行預編譯,並且SQL語句可以使用’?'佔位符來代替其中的參數,支持帶有參數的SQL的執行。

​ 對於SQL中的’?'佔位符,可以通過PreparedStatement接口提供的setter方法來設置入參的值,但是設置時必須要指定與輸入參數已定義SQL類型兼容的類型。理解起來有點繞,我們舉個例子:如果入參的SQL類型爲Integer,則應該調用setInt方法來設置入參。

​ 下面我們一起來看下PreparedStatement接口中定義的常用的幾種方法:

方法名 功能描述
ResultSet executeQuery() 在PreparedStatement對象中執行SQL查詢語句,並返回此次查詢生成的ResultSet對象
int executeUpdate() 在對象中執行SQL語句,如INSERT、UPDATE、DELETE或者無返回的SQL語句;如果爲改變任何數據或者無返回值則返回0
boolean execute() 執行給定的SQL,返回執行一個boolean類型的結果,通過getResultSet、getUpdateCount方法獲取執行結果
void setXxx(int parameterIndex, xxx x) 設置SQL語句中的入參,其中Xxx爲數據類型,如setSring、setBoolean等;其中的參數索引從1開始

PreparedStatement給定了設置所有SQL類型參數的方法,需要注意的是,setTime、setDate等方法,其中的Time、Date爲java.sql包中的,而不是我們常用的java.util包中的

​ 如何使用這些set方法呢,我們通過一個小栗子來簡單示範下:

PreparedStatement pstmt = con.prepareStatement("UPDATE EMPLOYEES SET " + 
                                               "SALARY = ? WHERE ID = ?");
pstmt.setBigDecimal(1, 153833.00)
pstmt.setInt(2, 110592)
//接收SQL語句執行結果
int result = pstmt.executeUpdate()

2.6 CallableStatement接口

CallableStatement接口是用於執行SQL存儲過程。JDBC API提供了一種存儲過程SQL轉義語法,該語法允許所有RDBMS可以通過標準方式調用存儲過程。並且CallableStatement接口繼承了PreparedStatement接口,因此其也可以執行非存儲過程的SQL語句。

​ 如果存儲過程中有參數(In,入參),可以通過集成自PreparedStatement接口中的setXxx方法來賦值。

​ 如果存儲過程有輸出參數(Out,出參),在調用存儲過程前必須通過registerOutParameter先註冊,存儲過程執行後可以通過getXxx方法來獲取結果。

​ 同樣的,同樣也可以將查詢結果放入ResultSet對象中,通過迭代的方式獲取執行結果。如果存儲過程返回多個結果集,需要通過調Statement對象中的getMoreResults方法切換到下一個結果集,並通過getResult方法獲取。

​ 存儲過程調用的示例代碼如下(在這裏簡單演示下,後續博客會詳細講解):

//第一個參數爲In參數,第二個爲Out參數
CallableStatement callableStatement = connection.prepareCall(
  "{ call queryUsersById(?,?)} ");
callableStatement.setString(1, "1");

//註冊返回參數
callableStatement.registerOutParameter(2, Types.CHAR);

//執行存儲過程
ResultSet rs = callableStatement.executeQuery();
//處理返回的結果集  如果返回多個結果集,這裏會獲取到第一個
while (rs.next()) {
  //...
}
//移動指針到下一個ResultSet對象
while (callableStatement.getMoreResults()) {
  rs = callableStatement.getResultSet();
  //...
}

CallableStatement接口中只定義了getXxx方法和registerOutParameter方法,因此這裏就不擴展開來講了,registerOutParameter方法有許多重載的方法,但是其功能都是進行輸出參數的註冊,這裏使用java.sql.Types來指定類型即可。

2.7 ResultSet接口

ResultSet對象用於保存通過執行查詢語句查詢數據庫產生的結果集,使用一個邏輯表格來表示一個數據庫查詢結果集。ResultSet接口內部有一個指向當前數據行的遊標(cursor),初始時,遊標指向第一行數據之前(可以理解成第0行),調用next方法可將遊標移動至下一行,如果下一行沒有數據(移動到了最後一行之後),則返回false(表示到了末尾),我們通常可以使用此方法來完成結果集的遍歷,代碼如下所示:

//...
ResultSet rs = stmt.executeQuery("SELECT * FROM TABLE2")
while(rs.next()){
  //處理邏輯
} 

默認的ResultSet對象是不可更新的,並且遊標只能向後(next)移動。 因此,在程序中只能迭代ResultSet一次,並且只能從第一行到最後一行進行迭代。這樣顯然不是JDBC的做事風格,雖然一個next方法足夠我們使用了,但是JDBC還是爲我們提供了更豐富的功能。我們首先來看下,除了next方法外,其他的遊標移動方法。

資源分配圖
方法名 功能描述
boolean next() 將遊標移動到下一行,當移動到數據最後一行之後時,返回false
boolean previous() 將遊標移動到上一行,當移動到數據第一行之前時,返回false
boolean relative(int rows) 將遊標移動相對的行數(正數或負數),正數向後、負數向前
boolean absolute( int row ) 將遊標移動到數據集中指定的行數
void beforeFirst() 將遊標移動到數據第一行之前,相當於absolute(0)
void afterLast() 將遊標移動到數據最後一行之後
boolean first() 將遊標移動到數據第一行
boolean last() 將遊標移動到數據最後一行

​ 我們在上一段中說了,默認的ResultSet對象是不可更新的,並且遊標只能向後(next)移動,那上面的這麼多方法,要如何才能使用呢?這裏需要我們在創建StatementPreparedStatement時指定結果集類型和併發類型,我們來看一下其中的幾個值(這幾個常量在java.sql.ResultSet中):

方法名 功能描述
TYPE_FORWARD_ONLY(1003) 結果集類型,只能向後移動
TYPE_SCROLL_INSENSITIVE(1004) 結果集類型,可滾動,但是對數據庫中的數據變更不care
TYPE_SCROLL_SENSITIVE(1005) 結果集類型,可滾動,並且數據庫中的數據變更會直接影響結果集
CONCUR_READ_ONLY(1007) 併發類型,只讀,結果集獲取後不會被更新
CONCUR_UPDATABLE(1008) 併發類型,結果集獲取後可能被更新

​ 因此我們在創建StatementPreparedStatement時指定這兩個參數即可,這裏推薦使用1004和1007,否則可能會發生你意想不到的錯誤,示例代碼如下。這樣創建的StatementPreparedStatement對象在執行查詢時返回的結果集,遊標就可以自由的飛翔了。

//也可直接使用con.createStatement(1004, 1008)
Statement pstmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet
                                      .CONCUR_READ_ONLY);

PreparedStatement pstmt = con.prepareStatement("your sql", 1004, 1008);

​ 當我們把遊標移動到數據行後,就可以使用JDBC提供的getXxx方法來獲取對應記錄中字段的值了。這裏的getXxx方法和PreparedStatement中的setXxx方法類似,會自動的將SQL數據類型和Java中的數據類型進行轉換。我們通過代碼簡單的看下如何使用

PreparedStatement pstmt = con.prepareStatement("select * from users");
// 獲取查詢結果集
resultSet = statement.executeQuery(sql);
//resultSet移動遊標(Cursor)到結果的第一行
while (resultSet.next()) {
  //通過getXxx方法獲取查詢結果
  System.out.println("name:" + resultSet.getString("name"));
  System.out.println("password:" + resultSet.getString("password"));
  System.out.println("birthday:" + resultSet.getDate("birthday"));
}

3.JDBC編程步驟

​ 講完了理論,讓我們來簡單的動動手,瞭解通過JDBC進行數據庫操作的步驟。

  1. 加載並註冊數據驅動

​ 我們可以通過顯示的註冊驅動,可通過如下三種方式:

//java.sql可省略,import正確的類即可,Driver爲數據庫驅動中實現了Driver接口的驅動類
java.sql.DriverManager.registerDriver(new Driver);

//將Driver類加載至內存,由其中的靜態方法自動完成註冊,xxx表示Driver類在connetor jar包中的位置
Class.forName("xxx.Driver")
  
//設置應用中的系統屬性jdbc.drivers,或者增加JVM的啓動參數
System.setProperty("jdbc.drivers", "xxxx.Driver");

​ 我們還可以什麼都不做,讓DriverManager自動幫我們註冊。

  1. 通過DriverManager獲取數據庫的連接Connection

​ 這裏同樣有三種方式來建立連接,代碼如下:

//第一種
String url = "jdbc:mysql://localhost:3306/java_web?useSSL=false&characterEncoding=utf-8";
String userName = "root";
String password = "123456";
Connection connection = DriverManager.getConnection(url, userName, password);

//第二種
//java.util.Properties
Properties properties = new Properties();
properties.put("user", userName);
properties.put("password", password);
//第一種和第二種本質上是相同的
connection = DriverManager.getConnection(url, properties);

//第三種 將用戶名密碼寫入url中
url = "jdbc:mysql://localhost:3306/java_web?user=root&password=123456" + 													"&useSSL=false&characterEncoding=utf-8";
connection = DriverManager.getConnection(url);
  1. 通過Connection對象獲取Statment對象或PreparedStatement對象或CallableStatement對象

​ 代碼如下,以下三個對象都是位於java.sql包中,import的時候需注意:

//獲取Statment對象
Statement statement = connection.createStatement();

//獲取PreparedStatement對象
PreparedStatement preparedStatement = connection.prepareStatement("your sql");

//獲取CallableStatement對象
CallableStatement callableStatement =  connection.prepareCall("your sql");
  1. 使用獲取的Statment對象執行SQL語句(以Statment爲例)

​ 可使用如下三種方式執行SQL語句:

  • boolean execute():可以執行任何SQL語句;
  • ResultSet executeQuery(String sql):通常執行查詢(SELECT)語句,執行後返回代表結果集的ResultSet對象;
  • int executeUpdate(String sql):主要用於執行DML、DDL語句,返回數據庫中此次執行影響的行數,如果執行DDL語句(無返回),則返回0
  1. 處理返回結果

​ 這裏的返回結果分爲三種,也就是上面執行SQL語句的三種方式的返回值。我們需要獲取SQL的執行結果,並完成解析,之後完成項目自身的業務邏輯。

ResultSet的解析在上文已經講解過了,故不在贅述。

  1. 關閉連接,釋放資源

​ 需要執行的SQL執行完畢,即此次的數據庫操作已經完成,我們需要關閉數據庫連接、Statement對象、ResultSet對象。

​ 代碼如下:

//關閉連接,釋放資源
public static void release(ResultSet resultSet, Statement statement, Connection connection) {
  //關閉結果集
  if (resultSet != null) {
    try {
      resultSet.close();
    } catch (SQLException e) {
      e.printStackTrace();
    }
    resultSet = null;
  }
  //關閉Statement對象
  if (statement != null) {
    try {
      statement.close();
    } catch (SQLException e) {
      e.printStackTrace();
    }
    statement = null;
  }
  //關閉Connection連接
  if (connection != null) {
    try {
      connection.close();
    } catch (SQLException e) {
      e.printStackTrace();
    }
    connection = null;
  }
}

​ 一個完成的操作實例可以參考這篇文章:Java Web數據庫開發(MySQL)之環境準備

4.總結

​ 本文到這,JDBC中的重要的接口和類都已講解完畢,後續會單獨開文章來講解Statment對象、PreparedStatement對象、CallableStatement對象的具體使用。

​ 因爲本文主要是對JDBC的詳細介紹,只稍微的提到了MySQL Connector中的Driver類,後續有機會會單獨的寫文章進行MySQL Connectior的源碼分析。

參考閱讀:

  1. Java Web數據庫開發(MySQL)之環境準備
  2. MySQL Connector源碼下載地址
  3. mysql-connector jar包及包含源碼的jar包的下載地址

​ 又到了分隔線以下,本文到此就結束了,本文內容全部都是由博主自己進行整理並結合自身的理解進行總結,如果有什麼錯誤,還請批評指正。

​ Java web這一專欄會是一個系列博客,喜歡的話可以持續關注,如果本文對你有所幫助,還請還請點贊、評論加關注。

​ 有任何疑問,可以評論區留言。

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