Spring JdbcTemplate 調用 Oracle 存儲過程 與 Oracle 驅動下載

目錄

前  言

Oracle 驅動下載

Oracle 數據源配置

execute 調用無返回值存儲過程

execute 調用單個返回值存儲過程

execute 調用返回結果集存儲過程

call 方法調用存儲過程


前  言

1、關於 JdbcTemplate 的介紹、pom 依賴、DI 注入可以參考《Spring JdbcTemplate 模板剖析 之 常用 增刪改查》,本文繼續介紹 JdbcTemplate 調用數據庫的存儲過程,雖然 Mysql 也有存儲過程,但是爲了儘可能的多覆蓋一點,本文選擇調用 Oracle 的存儲過程,其它數據庫也是同理。

1)execute 方法:能執行任何 SQL 語句,一般用於執行 DDL 語句;以及 建表、刪表等等 SQL.
2)update、batchUpdate 方法:update 方法用於執行新增、修改、刪除等語句;batchUpdate 方法用於執行批處理相關語句;
3)queryForObject、queryForList、query 方法:用於執行查詢相關語句;queryForObject 查詢的結果只能是1條,多餘或少於都會拋異常;
4)queryForList 與 query 查詢結果爲空時,返回的 list 大小爲0,不會引發空指針異常。
5)call 方法:用於執行存儲過程、函數相關語句。

2、本文環境:Oracle 11g(驅動版本 ojdbc8-19.3.0.0) + Java JDK 1.8 + Spring Boot 2.1.5 +  + IDEA 2018.

3、JdbcTemplate 本身就是對 JDBC 的輕量級封裝,所以調用存儲過程也類似 "JDBC 調用存儲過程/函數"。

4、爲了測試方便,先提前準備數據:準備員工表與部門表測試數據

Oracle 驅動下載

1、不同於開源的 Mysql ,Oracle 是收費的,從 Maven 中央倉庫上面通常無法成功下載 Oracle 依賴,只能直接去 Oracle 官網下載:

<!-- https://mvnrepository.com/artifact/com.oracle.jdbc/ojdbc8 -->
<!-- oracle 官網提供的驅動依賴,通常都會下載失敗,需要手動在本地倉庫進行 mvn install 安裝-->
 <dependency>
     <groupId>com.oracle.jdbc</groupId>
     <artifactId>ojdbc8</artifactId>
     <version>12.2.0.1</version>
 </dependency>

ojdbc6 匹配 jdk 1.6、ojdbc8 匹配 jdk 1.8,ojdbc10 匹配 jdk 10 依此類推,而 ojdbc14 匹配的是 jdk1.4.

2、解決方式一:網絡上有好心人和組織共享了一些可供下載的、且與官網等價的 maven 依賴,比如下面這個(親測有效):

<!-- https://mvnrepository.com/artifact/com.github.noraui/ojdbc8 -->
<!--這是網絡上的雷鋒提供的開源 ojdbc8 驅動,功能與官網的是一樣的,專門用於替代 Oracle 官網 maven 下載失敗-->
<dependency>
    <groupId>com.github.noraui</groupId>
    <artifactId>ojdbc8</artifactId>
    <version>12.2.0.1</version>
</dependency>

3、解決方式二:雖然從 maven 中央倉庫無法下載 ojdbc 驅動,但是直接從 Oracle 官網是可以下載 jar 包的。

3.1、先從官網下載 ojdbc8.jar https://www.oracle.com/database/technologies/jdbc-ucp-122-downloads.html

從 Oracle 官網下載它們家的東西都是需要先登陸的,所以如果沒有賬號,需要先註冊,下載驅動不收費。

3.3、然後使用下面的 mvn install 命令進行項目部署,它會自動存放在本地倉庫中:

mvn install:install-file -DgroupId=com.oracle.jdbc -DartifactId=ojdbc8 -Dversion=12.2.0.1 -Dpackaging=jar -DgeneratePom=true -Dfile=C:\Users\Think\Downloads\ojdbc8.jar

-DgroupId:指定 groupId 的值,可以自定義,建議與人家官網的一致即可
-DartifactId:指定 artifactId 的值,可以自定義,建議與人家官網的一致即可
-Dversion:指定 version 的值,可以自定義,建議與人家官網的一致即可,也可以從打開 ojdbc8.jar ,其中的 MANIFEST.MF 文件中也可以看到版本號
-Dpackaging:指定打包的類型,如 jar、war 包
-DgeneratePom:指定是否生成 pom.xml 文件,指定讓它生成
-Dfile:需要部署的 jar 包文件路徑

Oracle 數據源配置

1、在開始下面的代碼編寫之前,需要在全局配置文件中指定數據源配置:

#數據源配置
spring:
  profiles: oracleDb
  datasource:
    username: hnbs_3
    password: 1
    driverClassName: oracle.jdbc.driver.OracleDriver
    url: jdbc:oracle:thin:@127.0.0.1:1521:ORCL

更多配置項可以參考官網:https://docs.spring.io/spring-boot/docs/2.1.6.RELEASE/reference/htmlsingle/#common-application-properties

或者:org.springframework.boot.autoconfigure.jdbc.DataSourceProperties.java

execute 調用無返回值存儲過程

1、Mysql 中有 "drop table if exists 表名"的操作,當表存在時才進行刪除,Oracle 中並沒有 if exists 的判斷,所以直接刪除時,如果表不存在,"drop table 表名 " 就會報錯,

2、這裏用一個存儲過程來解決這個問題,Oracle 數據庫中準備存儲過程如下,功能就是傳入表名作爲參數,如果表已經存在,則刪除它,否則不進行操作,存儲過程不進行返回。

--表名作爲參數,如果表已經存在,則刪除它。
create or replace procedure pro_drop_table_by_name(tableName in user_tables.TABLE_NAME%type)
is
  flag number := 0; --表是否存在的表示,大於0表示表已經存在
begin
    --user_tables 是系統定義的視圖,可以查看當前用戶下的所有表信息,表中的表名區分大小寫,而且是大寫
    select count(1) into flag from user_tables where table_name = upper(tableName) ;
    if flag > 0 then
        execute immediate 'drop table '|| tableName ;--如果表已經存在,則刪除它
    end if;
end;

-- 數據庫中調用存儲過程:call pro_drop_table_by_name('student');

注意:這裏爲了保證數據完整性,並沒有做級聯刪除,也就是說當被刪除的表中的數據如果被其它表引用,則刪除時會報錯

    /**
     * 刪除表
     * http://localhost:8080/emp/dropTable?tableName=emp
     * http://localhost:8080/emp/dropTable?tableName=dept
     * emp 員工表引用了 dept 部門表,如果先刪除 dept 表,其中有數據被 emp 表引用,則刪除時報錯:
     * oracle.jdbc.OracleDatabaseException: ORA-02449: 表中的唯一/主鍵被外鍵引用
     *
     * @param tableName
     * @return
     */
    @GetMapping("emp/dropTable")
    public String dropTableByName(@RequestParam String tableName) {
        JsonObject jsonObject = new JsonObject();
        try {
            //sql 和在數據庫中完全一樣
            String sql = "call pro_drop_table_by_name('" + tableName + "')";
            jdbcTemplate.execute(sql);

            jsonObject.addProperty("code", 200);
            jsonObject.addProperty("message", sql);
        } catch (DataAccessException e) {
            logger.error(e.getMessage(), e);
            jsonObject.addProperty("code", 500);
            jsonObject.addProperty("message", e.getMessage());
        }
        return jsonObject.toString();
    }

execute 調用單個返回值存儲過程

1、Oracle 數據庫中準備存儲過程如下:

--表名作爲參數,同時指定返回參數,如果表名存在,則返回 1,不存在返回 0
create or replace procedure pro_check_table_by_name(tableName in user_tables.TABLE_NAME%type, ifExists out number) is
begin
    --user_tables 是系統定義的視圖,可以查看當前用戶下的所有表信息,表中的表名區分大小寫,而且是大寫
    select count(1) into ifExists from user_tables where table_name = upper(tableName) ;
end;

-- 數據庫中調用存儲過程:
declare
   tableName varchar2(30) := 'demp'; //被檢查的表名
   ifExists number; //返回參數
begin
  pro_check_table_by_name(tableName,ifExists);
  dbms_output.put_line(ifExists);//打印返回值
end;

2、execute(CallableStatementCreator csc, CallableStatementCallback<T> action) 調用存儲過程底層就是"JDBC 調用存儲過程/函數",所以寫起來完全一樣,CallableStatementCreator  中創建 java.sql.CallableStatement,CallableStatementCallback 中進行調用以及獲取返回值。

/**
 * 檢查某個表在數據庫中是否已經存在,存在時返回1,否則返回0
 * http://localhost:8080/emp/checkTableByName?tableName=emp
 *
 * @param tableName
 * @return
 */
@GetMapping("emp/checkTableByName")
public Integer checkTableByName(@RequestParam String tableName) {
	Integer execute = (Integer) jdbcTemplate.execute(new CallableStatementCreator() {

		//創建可回調語句,方法裏面就是純 jdbc 創建調用存儲的寫法
		@Override
		public CallableStatement createCallableStatement(Connection connection) throws SQLException {
			//存儲過程調用 sql,通過 java.sql.Connection.prepareCall 獲取回調語句
			String sql = "call pro_check_table_by_name(?,?)";
			CallableStatement callableStatement = connection.prepareCall(sql);
			//設置第一個佔位符參數值,傳入參數。參數索引從1開始
			callableStatement.setString(1, tableName);
			//註冊第二個參數(返回值)的數據類型
			callableStatement.registerOutParameter(2, OracleTypes.INTEGER);
			return callableStatement;
		}
	}, new CallableStatementCallback<Object>() {
		//正式調用存儲過程以及處理返回的值.
		@Override
		public Object doInCallableStatement(CallableStatement callableStatement) throws SQLException {
			//執行調用存儲過程
			callableStatement.execute();
			//參數索引從1開始,獲取村存儲過程的返回值.
			return callableStatement.getInt(2);
		}
	});
	return execute;
}

execute 調用返回結果集存儲過程

1、數據中準備存儲過程如下:


--創建存儲過程,用於分頁查詢
--傳入參數:pageNo 查詢的頁碼,pageSize 每頁的條數;輸出參數:vrows 使用一個引用遊標用於接收多條結果集。普通遊標無法做到,只能使用引用遊標
create or replace procedure pro_query_emp_limit(pageNo in number,pageSize in number,vrows out sys_refcursor) is
begin
  --存儲過程中只進行打開遊標,將 select 查詢出的所有數據放置到 vrows 遊標中,讓調用着進行獲取
open vrows for select t.empno,t.ename,t.job,t.mgr,t.hiredate,t.sal,t.comm,t.deptno from (select rownum r,t1.* from emp t1) t 
     where t.r between ((pageNo-1) * pageSize+1) and pageNo * pageSize;
end;

--數據庫中使用引用遊標讀取上面的存儲過程返回的值。下面只是加深理解,和 java 調用無關
declare
     vrows sys_refcursor ;--聲明引用遊標
     vrow emp%rowtype; --定義變量接收遍歷到的每一行數據
begin
     pro_query_emp_limit(5,3,vrows);--調用存儲過程
     loop 
       fetch vrows into vrow; -- fetch into 獲取遊標的值
       exit when vrows%notfound; -- 如果沒有獲取到值,則退出循環
       dbms_output.put_line('姓名:'|| vrow.ename || ' 薪水:'|| vrow.sal);
     end loop;
end;

2、調用和上面的單挑結果返回基本一致,區別就是將返回的結果改成 ResultSet 結果集:

/**
 * 存儲過程實現分頁查詢,傳入頁碼和條數即可進行分頁返回
 * http://localhost:8080/emp/pageQuery?pageNo=2&pageSize=5
 *
 * @param pageNo   頁碼
 * @param pageSize 每頁顯示的條數
 * @return
 */
@GetMapping("emp/pageQuery")
public List pageQuery(@RequestParam Integer pageNo, @RequestParam Integer pageSize) {
	List execute = (List) jdbcTemplate.execute(new CallableStatementCreator() {

		//創建可回調語句,方法裏面就是純 jdbc 創建調用存儲的寫法
		@Override
		public CallableStatement createCallableStatement(Connection connection) throws SQLException {
			//存儲過程調用 sql,通過 java.sql.Connection.prepareCall 獲取回調語句,sql 外圍可以花括號括起來
			String sql = "{call pro_query_emp_limit(?,?,?)}";
			CallableStatement callableStatement = connection.prepareCall(sql);
			//設置第佔位符參數值
			callableStatement.setInt(1, pageNo);
			callableStatement.setInt(2, pageSize);
			//輸出參數類型設置爲引用遊標
			callableStatement.registerOutParameter(3, OracleTypes.CURSOR);
			return callableStatement;
		}
	}, new CallableStatementCallback<Object>() {
		//正式調用存儲過程以及處理返回的值.
		@Override
		public Object doInCallableStatement(CallableStatement callableStatement) throws SQLException {
			//存儲返回結果
			List<Map<String, Object>> resultMapList = new ArrayList<>(8);
			//遍歷時臨時對象
			Map<String, Object> temp;
			//執行調用存儲過程,將結果轉爲 java.sql.ResultSet 結果集
			callableStatement.execute();
			ResultSet resultSet = (ResultSet) callableStatement.getObject(3);

			//遍歷結果集
			while (resultSet.next()) {
				temp = new HashMap<>(8);
				//根據字段名稱取值
				temp.put("empno", resultSet.getInt("empno"));
				temp.put("ename", resultSet.getString("ename"));
				temp.put("job", resultSet.getString("job"));
				temp.put("mgr", resultSet.getInt("mgr"));
				temp.put("hiredate", resultSet.getDate("hiredate"));
				temp.put("sal", resultSet.getFloat("sal"));
				resultMapList.add(temp);
			}
			return resultMapList;
		}
	});
	return execute;
}

call 方法調用存儲過程

1、開篇就已經說過 call 方法專門用於執行存儲過程、函數相關語句。call 方法在 execute 的基礎上對返回結果進行進一步的封裝,只需要創建 CallableStatement 即可,不用再關心結果轉換。

2、exexute 的 CallableStatementCallback 回調改爲使用 List<SqlParameter>,其中的每一個 SqlParameter 按順序對應占位符參數。

SqlParameter 表示存儲過程的傳入參數,可以不指定參數名稱,但是必須指定參數類型
SqlOutParameter 表示存儲過程的輸出參數,必須指定名稱和類型,名稱自定義即可,會被作爲返回值存放在 map 中

3、下面改用 call 方法來實現上面的功能,使用存儲過程檢查某個表在數據庫中是否已經存在,存在時返回1,否則返回0,使用 call 方法進行調用:

/**
 * 存儲過程檢查某個表在數據庫中是否已經存在,存在時返回1,否則返回0,使用 call 方法進行調用
 * http://localhost:8080/emp/callCheckTableByName?tableName=emp
 *
 * @param tableName
 * @return
 */
@GetMapping("emp/callCheckTableByName")
@SuppressWarnings("all")
public Map<String, Object> callCheckTableByName(@RequestParam String tableName) {

	//SqlParameter 表示存儲過程的傳入參數,可以不指定參數名稱,但是必須指定參數類型
	//SqlOutParameter 表示存儲過程的輸出參數,必須指定名稱和類型,名稱自定義即可,會被作爲返回值存放在 map 中
	List<SqlParameter> sqlParameterList = new ArrayList<>(4);
	sqlParameterList.add(new SqlParameter(OracleTypes.VARCHAR));
	sqlParameterList.add(new SqlOutParameter(tableName, OracleTypes.NUMBER));

	//call 方法在 execute 的基礎上對返回結果進行進一步的封裝,只需要創建 CallableStatement
	//List<SqlParameter> 中的每一個 SqlParameter 按順序對應占位符參數
	//返回的 map 包含返回參數
	Map<String, Object> call = jdbcTemplate.call(new CallableStatementCreator() {
		@Override
		public CallableStatement createCallableStatement(Connection connection) throws SQLException {
			//存儲過程調用 sql,通過 java.sql.Connection.prepareCall 獲取回調語句,sql 外圍可以花括號括起來
			String sql = "{call pro_check_table_by_name(?,?)}";
			CallableStatement callableStatement = connection.prepareCall(sql);
			//設置第一個佔位符參數值,傳入參數。參數索引從1開始
			callableStatement.setString(1, tableName);
			//註冊第二個參數(返回值)的數據類型,oracle.jdbc.OracleTypes 中定義了全部的數據類型常量
			callableStatement.registerOutParameter(2, OracleTypes.INTEGER);
			return callableStatement;
		}
	}, sqlParameterList);
	return call;
}

4、使用 call 方法調用存儲過程進行分頁查詢,使用了 call 之後對於返回的遊標就方便多了,不再需要自己一個一個取值了,它會自動進行轉換,推薦方式:

/**
 * 使用 call 方法調用存儲過程進行分頁查詢,推薦方式
 * http://localhost:8080/emp/callPageQuery?pageNo=2&pageSize=5
 *
 * @param pageNo
 * @param pageSize
 * @return
 */
@GetMapping("emp/callPageQuery")
@SuppressWarnings("all")
public List<Map<String, Object>> callPageQuery(@RequestParam Integer pageNo, @RequestParam Integer pageSize) {
	//設置存儲過程參數
	//SqlParameter 表示存儲過程的傳入參數,可以不知道參數名稱,但是必須指定參數類型
	//SqlOutParameter 表示存儲過程的輸出參數,必須指定名稱和類型,名稱自定義即可,會被作爲返回值存放在 map 中
	List<SqlParameter> sqlParameterList = new ArrayList<>(4);
	sqlParameterList.add(new SqlParameter(OracleTypes.NUMBER));
	sqlParameterList.add(new SqlParameter(OracleTypes.NUMBER));
	sqlParameterList.add(new SqlOutParameter("resultSet", OracleTypes.CURSOR));

	//使用了 call 之後對於返回的遊標就方便多了,不再需要自己一個一個取值了,它會自動進行轉換
	//call 的 key 會是 resultSet,然後它的值會是一個 List<Map>,自動轉換好了
	Map<String, Object> call = jdbcTemplate.call(new CallableStatementCreator() {
		//創建可回調語句,方法裏面就是純 jdbc 創建調用存儲的寫法
		@Override
		public CallableStatement createCallableStatement(Connection connection) throws SQLException {
			//存儲過程調用 sql,通過 java.sql.Connection.prepareCall 獲取回調語句,sql 外圍可以花括號括起來
			String sql = "{call pro_query_emp_limit(?,?,?)}";
			CallableStatement callableStatement = connection.prepareCall(sql);
			//設置第佔位符參數值
			callableStatement.setInt(1, pageNo);
			callableStatement.setInt(2, pageSize);
			//輸出參數類型設置爲引用遊標
			callableStatement.registerOutParameter(3, OracleTypes.CURSOR);
			return callableStatement;
		}
	}, sqlParameterList);

	//沒有值時就是空 list,不會控制在異常
	List<Map<String, Object>> dataList = (List<Map<String, Object>>) call.get("resultSet");
	return dataList;
}

源碼:https://github.com/wangmaoxiong/jdbc_template_app

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