JDBC之CallableStatement執行存儲過程

​ 在前面的一篇文章中,我們學習使用Statement、PreparedStatement來完成對數據表的增刪改查。而存儲過程作爲數據庫的重要組成部分(痛點,當時學的時候頭髮都掉了好幾根😭),那JDBC是如何執行存儲過程呢?今天我們就來看看如何使用CallableStatement接口來執行存儲過程。(本文使用的數據庫爲MySQL)。

​ 興奮、緊張,迫不及待,又要學新知識了。

資源分配圖

1.存儲過程簡介

​ 在開始講如何執行存儲過程之前,我們先來簡單的看下存儲過程的相關概念。

存儲過程(Stored Procedure)是在大型數據庫系統中,一組爲了完成特定功能的SQL 語句集,它存儲在數據庫中,一次編譯後永久有效,用戶通過指定存儲過程的名字並給出參數(如果該存儲過程帶有參數)來執行它。存儲過程是數據庫中的一個重要對象。

​ 上面的釋義爲百度百科上的詞條,講的也非常清晰了。我們在開發的過程中,經常在一個功能中,會執行多條SQL語句,這時就可以將這一組SQL語句封裝在一個存儲過程中,在應用端就可以通過調用存儲過程來獲取對應的結果,而不用執行許多SQL了。

這裏說明一件事,關於是否應使用存儲過程的爭論由來已久,我寫這篇文章也沒有任何誘導你的想法,是否使用存儲過程還需你自行評估。

​ 這裏簡單的講下存儲過程的優缺點:

​ 優點:1.執行速度快:存儲過程預先創建好的,並且預先編譯了,省去了每條SQL編譯的時間;2.安全性高:可以避免SQL注入問題,避免暴露表結構和字段。

​ 缺點:1.對數據庫依賴性比較大,數據庫遷移改動會非常大;2.不適用於數據庫集羣,存儲過程依賴於具體的庫來實現的,如果數據是分佈存儲在多個庫中,存儲過程就很難處理了;3.可維護性差、可讀性差:程序員無法直接看到代碼,業務發生變更時不利於快速開發。

2.CallableStatement接口

​我們來看一張圖:

資源分配圖

​ 從上圖可以看到,CallableStatement接口繼承(extends)了PreparedStatement接口,也就是說,CallableStatement接口不僅可以執行存儲過程,還可以使用PreparedStatement接口提供方法的執行DQL、DDL和DML語句。

​ 存儲過程可能會包含入參(IN),出參(OUT),出入參(INOUT,既可做入參又可做出參),執行的結果也會返回int或是ResultSet,下面我們一起來看下如何使用CallableStatement來執行存儲過程。

3.執行無參的存儲過程

​ 環境搭建請參考前面的文章,這裏數據庫中有一個users表,我們就簡單的操作這張表來演示存儲過程的使用。

​ 首先我們創建一個無參的存儲過程,SQL如下,邏輯很簡單,就是查詢users表中的所有用戶信息。

CREATE PROCEDURE SelectAllUsers()
BEGIN
	SELECT * FROM users;
END;

​ 下面我們來看下JDBC是怎麼執行無參的存儲過程的,代碼如下,其中的JDBCUtil代碼參考上文

/**
	 * 調用無參的存儲過程,返回users表中的所有數據
	 * 
	 * @return
	 * @throws ClassNotFoundException
	 * @throws SQLException
	 */
public List<User> callNoParamProcedure() throws ClassNotFoundException, SQLException {
  Connection connection = null;
  CallableStatement statement = null;
  ResultSet resultSet = null;
  try {
    // 獲取數據庫連接
    connection = JDBCUtil.getConnection();
    // 構建Sql
    String sql = "{ call SelectAllUsers() }";
    // 創建CallableStatement對象
    statement = connection.prepareCall(sql);
    // 執行存儲過程
    resultSet = statement.executeQuery();
    // 迭代獲取所有的用戶
    while (resultSet.next()) {
      // new一個User實例
      User user = new User();
      user.setId(resultSet.getInt("id"));
      user.setUserName(resultSet.getString("name"));
      user.setPassword(resultSet.getString("password"));
      user.setEmail(resultSet.getString("email"));
      user.setBirthday(resultSet.getDate("birthday"));
      // 輸出用戶信息
      System.out.println("獲取的用戶信息爲:" + user);
    }
    return userList;
  } catch (ClassNotFoundException e) {
    e.printStackTrace();
    throw e;
  } catch (SQLException e) {
    e.printStackTrace();
    throw e;
  } finally {
    JDBCUtil.release(resultSet, statement, connection);
  }
}

​ 上述代碼執行後的運行結果如下:

資源分配圖

4.執行有入參的存儲過程

​ 上面我們講了,存儲過程可能會包含IN、OUT、INOUT參數,這裏我們需要注意下,當存儲過程中有OUT參數事,在執行存儲過程前需要通過CallableStatement中的registerOutParameter方法先註冊。

​ 這裏創建如下存儲過程:

CREATE PROCEDURE SelectNameByIdWithOut(IN in_id int, OUT out_name CHAR(50))
BEGIN
	SELECT `name` into out_name from users where id = in_id;
	SELECT * FROM users WHERE id = in_id;
END;

​ 代碼如下,:

**
  * 調用含參的存儲過程, 返回users表中id對應的數據
  * 
  * @return
  * @throws ClassNotFoundException
  * @throws SQLException
  */
public void callInOutParamProcedure(int id) throws ClassNotFoundException, SQLException {
  Connection connection = null;
  CallableStatement statement = null;
  ResultSet resultSet = null;
  try {
    // 獲取數據庫連接
    connection = JDBCUtil.getConnection();
    // 構建Sql
    String sql = "{call SelectNameByIdWithParam(?,?)}";
    // 創建CallableStatement對象
    statement = connection.prepareCall(sql);
    // 設置IN參數的值
    statement.setInt(1, id);
    // 註冊OUT參數
    statement.registerOutParameter(2, Types.VARCHAR);
    // 執行存儲過程
    resultSet = statement.executeQuery();
    // 獲取上面註冊的OUT參數,這裏的index和入參順序一致
    System.out.println("Out參數返回的結果爲:" + statement.getString(2));
    // 迭代獲取用戶
    while (resultSet.next()) {
      User user = new User();
      user.setId(resultSet.getInt("id"));
      user.setUserName(resultSet.getString("name"));
      user.setPassword(resultSet.getString("password"));
      user.setEmail(resultSet.getString("email"));
      user.setBirthday(resultSet.getDate("birthday"));
      // 輸出用戶信息
      System.out.println("獲取的用戶信息爲:" + user);
    }
  } catch (ClassNotFoundException e) {
    e.printStackTrace();
    throw e;
  } catch (SQLException e) {
    e.printStackTrace();
    throw e;
  } finally {
    JDBCUtil.release(resultSet, statement, connection);
  }
}

​ 執行測試代碼callInOutParamProcedure(2)的運行結果如下圖所示:

資源分配圖

5.執行返回多個結果集的存儲過程

​ 在上面兩個例子中,執行查詢語句,查詢的結果就是一個結果集,但是如果一個存儲過程中有多個查詢語句呢?我們在程序中要如何將所有的結果集都獲取到呢?

​ 這裏我們創建如下存儲過程:

CREATE PROCEDURE SelectUserWithMlutiRs(IN in_id int, OUT out_name CHAR(50))
BEGIN
	SELECT `name` into out_name from users where id = in_id;
	SELECT * FROM users WHERE id = in_id;
	SELECT * FROM users;
END;

​ 我們現在mysql中執行此存儲過程(可以在WorkBench或者Navicat中,執行SQL即可,或者在MySQL客戶端中執行),代碼如下:

set @in_out='test-2';
CALL SelectUserWithMlutiRs(2, @in_out);
SELECT @in_out

​ 上述SQL的執行結果如下,其中結果1、結果1(2)爲執行存儲過程返回的結果集,可以看到,SelectUserWithMlutiRs返回了兩個結果集。

資源分配圖

​ 那我們在應用程序中又要怎麼獲取呢?

​ 這裏我們需要通過調Statement對象中的getMoreResults方法切換到下一個結果集,並通過getResult方法獲取。我們來看下代碼:

/**
	 * 調用含參的存儲過程, 返回users表中id對應的數據
	 * 
	 * @param id
	 * @throws ClassNotFoundException
	 * @throws SQLException
	 */
public void callMultiRsProcedure(int id) throws ClassNotFoundException, SQLException {
  Connection connection = null;
  CallableStatement statement = null;
  ResultSet resultSet = null;
  try {
    // 獲取數據庫連接
    connection = JDBCUtil.getConnection();
    // 構建Sql
    String sql = "{ call SelectUserWithMlutiRs(?,?) }";
    // 創建CallableStatement對象
    statement = connection.prepareCall(sql);
    // 設置IN參數的值
    statement.setInt(1, id);
    // 註冊OUT參數
    statement.registerOutParameter(2, Types.VARCHAR);
    // 執行存儲過程
    resultSet = statement.executeQuery();
    // 獲取上面註冊的OUT參數,這裏的index和入參順序一致
    System.out.println("Out參數返回的結果爲:" + statement.getString(2));
    int i = 1;
    System.out.println("第" + i + "個結果集中的數據爲:");
    while (resultSet.next()) {
      User user = new User();
      user.setId(resultSet.getInt("id"));
      user.setUserName(resultSet.getString("name"));
      user.setPassword(resultSet.getString("password"));
      user.setEmail(resultSet.getString("email"));
      user.setBirthday(resultSet.getDate("birthday"));
      System.out.println("獲取的用戶信息爲:" + user);
    }
    i++;
    // 判斷是否還有下一個結果集
    // 注意,此處不能直接調用,需先獲取當前的ResultSet纔可調用,否則上個結果集會丟失
    while (statement.getMoreResults()) {
      resultSet = statement.getResultSet();
      // 迭代獲取用戶
      // 迭代獲取用戶
      System.out.println("第" + i + "個結果集中的數據爲:");
      // 判斷是否還有下一個結果集
      // 注意,此處不能直接調用,需先獲取當前的ResultSet纔可調用,否則上個結果集會丟失
      while (resultSet.next()) {
        User user = new User();
        user.setId(resultSet.getInt("id"));
        user.setUserName(resultSet.getString("name"));
        user.setPassword(resultSet.getString("password"));
        user.setEmail(resultSet.getString("email"));
        user.setBirthday(resultSet.getDate("birthday"));
        System.out.println("獲取的用戶信息爲:" + user);
      }
      i++;
    }
  } catch (ClassNotFoundException e) {
    e.printStackTrace();
    throw e;
  } catch (SQLException e) {
    e.printStackTrace();
    throw e;
  } finally {
    JDBCUtil.release(resultSet, statement, connection);
  }
}

​ 執行測試代碼callMultiRsProcedure(3)的運行結果如下如所示:

資源分配圖

6.總結

​ 本文是對如何使用CallableStatement接口調用存儲過程的簡單示例。存儲過程因爲其自身優缺點非常明顯,具體是否需要使用還需自己進行評估。

參考閱讀:

  1. 減少存儲過程封裝業務邏輯-web開發與傳統軟件開發的思維模式不同
  2. 支付寶數據庫架構師馮大輝:談數據庫架構

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

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

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

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