杰瑞科技汇

Oracle存储过程如何与Java交互?

目录

  1. 核心概念

    Oracle存储过程如何与Java交互?-图1
    (图片来源网络,侵删)
    • JDBC 是什么?
    • Oracle 存储过程的几种类型
    • Java 调用存储过程的核心步骤
  2. 环境准备

    • Java 开发环境
    • Oracle JDBC 驱动 (ojdbc.jar)
    • Oracle 数据库和存储过程
  3. 详细代码示例

    • 示例 1:无参数的存储过程
    • 示例 2:带 IN 参数的存储过程
    • 示例 3:带 OUT 参数的存储过程
    • 示例 4:带 IN OUT 参数的存储过程
    • 示例 5:返回结果集的存储过程 (最复杂也最常用)
  4. 最佳实践与注意事项

    • 使用 try-with-resources 管理资源
    • 使用 PreparedStatement 防止 SQL 注入
    • 处理大数据类型 (CLOB, BLOB)
    • 事务管理

核心概念

JDBC (Java Database Connectivity)

JDBC 是 Java 语言用来规范客户端程序如何访问数据库的应用程序接口,它提供了查询和更新数据库中数据的方法,简单说,Java 和数据库之间的“翻译官”。

Oracle存储过程如何与Java交互?-图2
(图片来源网络,侵删)

Oracle 存储过程的几种类型

在 Java 中调用时,我们需要区分参数的类型:

  1. IN 参数:默认类型,数据从 Java 应用程序传递到存储过程,存储过程可以读取但不能修改这个值。
  2. OUT 参数:数据从存储过程传递回 Java 应用程序,调用前需要先注册,存储过程会为这个参数赋值。
  3. IN OUT 参数:兼具 IN 和 OUT 的功能,数据可以从 Java 传入,存储过程可以修改,然后将修改后的值传回 Java。

Java 调用存储过程的核心步骤

无论调用哪种类型的存储过程,基本流程都相似:

  1. 加载驱动:加载 Oracle JDBC 驱动类。
  2. 获取连接:使用 DriverManager 获取到数据库的 Connection 对象。
  3. 创建 CallableStatement:这是关键一步,使用 Connection 对象的 prepareCall() 方法,并传入一个调用存储过程的 SQL 字符串,这个字符串的格式是 {call procedure_name(?, ?, ...)}
  4. 设置参数
    • 对于 IN 参数,使用 setXxx() 方法(如 setString(), setInt())设置值。
    • 对于 OUT 参数,必须使用 registerOutParameter() 方法注册其数据类型。
    • 对于 IN OUT 参数,需要先 registerOutParameter,再 setXxx
  5. 执行:调用 CallableStatementexecute()executeUpdate() 方法。
  6. 获取结果
    • 对于 OUT 参数,使用 getXxx() 方法(如 getString(), getInt())获取返回值。
    • 对于返回结果集的存储过程,需要通过 CallableStatement.getObject() 获取 REF CURSOR,然后像处理普通 ResultSet 一样处理它。
  7. 关闭资源:关闭 CallableStatement, Connection 等对象,释放数据库连接。

环境准备

Oracle JDBC 驱动

你需要下载 Oracle JDBC 驱动,通常是一个名为 ojdbcX.jar 的文件,你可以从 Oracle 官网或 Maven 仓库获取。

使用 Maven (推荐): 在你的 pom.xml 中添加依赖:

Oracle存储过程如何与Java交互?-图3
(图片来源网络,侵删)
<dependency>
    <groupId>com.oracle.database.jdbc</groupId>
    <artifactId>ojdbc8</artifactId>
    <version>19.3.0.0</version> <!-- 请根据你的Oracle版本选择合适的版本 -->
</dependency>

数据库和存储过程示例

假设我们有一个 HR schema,下面是我们将要调用的几个存储过程。

-- 1. 无参数的存储过程:打印一条信息
CREATE OR REPLACE PROCEDURE say_hello AS
BEGIN
  DBMS_OUTPUT.PUT_LINE('Hello from Oracle Stored Procedure!');
END say_hello;
/
-- 2. 带IN参数的存储过程:根据ID查询员工姓名
CREATE OR REPLACE PROCEDURE get_employee_name (
  p_emp_id IN NUMBER,
  p_emp_name OUT VARCHAR2
)
AS
BEGIN
  SELECT first_name INTO p_emp_name FROM employees WHERE employee_id = p_emp_id;
EXCEPTION
  WHEN NO_DATA_FOUND THEN
    p_emp_name := 'Employee not found';
END get_employee_name;
/
-- 3. 带IN OUT参数的存储过程:计算两个数的和
CREATE OR REPLACE PROCEDURE add_numbers (
  p_num1 IN NUMBER,
  p_num2 IN NUMBER,
  p_sum OUT NUMBER
)
AS
BEGIN
  p_sum := p_num1 + p_num2;
END add_numbers;
/
-- 4. 返回结果集的存储过程:查询某个部门的员工
-- 注意:需要先创建一个包来定义REF CURSOR的类型
CREATE OR REPLACE PACKAGE emp_data_pkg AS
  TYPE emp_cursor_type IS REF CURSOR;
END emp_data_pkg;
/
CREATE OR REPLACE PROCEDURE get_employees_by_dept (
  p_dept_id IN NUMBER,
  p_emp_cursor OUT emp_data_pkg.emp_cursor_type
)
AS
BEGIN
  OPEN p_emp_cursor FOR
    SELECT employee_id, first_name, last_name
    FROM employees
    WHERE department_id = p_dept_id;
END get_employees_by_dept;
/

详细代码示例

我们将使用一个通用的数据库工具类来管理连接。

DatabaseUtil.java

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DatabaseUtil {
    private static final String URL = "jdbc:oracle:thin:@//localhost:1521/ORCLPDB1"; // 请替换为你的数据库URL
    private static final String USER = "hr"; // 请替换为你的用户名
    private static final String PASSWORD = "your_password"; // 请替换为你的密码
    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(URL, USER, PASSWORD);
    }
}

示例 1:无参数的存储过程

调用 say_hello

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.SQLException;
public class CallSimpleProcedure {
    public static void main(String[] args) {
        // 使用 try-with-resources 自动关闭资源
        try (Connection conn = DatabaseUtil.getConnection();
             CallableStatement cstmt = conn.prepareCall("{call say_hello}")) {
            // 执行存储过程
            cstmt.execute();
            System.out.println("Procedure 'say_hello' called successfully.");
            // 注意:如果存储过程使用DBMS_OUTPUT,需要在Java中启用并捕获输出
            // 这通常需要额外的配置,这里省略。
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

示例 2:带 IN 参数的存储过程

调用 get_employee_name,传入员工ID 101,获取姓名。

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Types;
public class CallProcedureWithInParam {
    public static void main(String[] args) {
        String sql = "{call get_employee_name(?, ?)}";
        try (Connection conn = DatabaseUtil.getConnection();
             CallableStatement cstmt = conn.prepareCall(sql)) {
            // 设置IN参数
            cstmt.setInt(1, 101); // 第一个参数是IN类型,设置ID为101
            // 注册OUT参数
            // 第二个参数是OUT类型,类型为VARCHAR,对应Types.VARCHAR
            cstmt.registerOutParameter(2, Types.VARCHAR);
            // 执行
            cstmt.execute();
            // 获取OUT参数的值
            String empName = cstmt.getString(2);
            System.out.println("Employee Name: " + empName);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

示例 3:带 IN OUT 参数的存储过程

调用 add_numbers

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Types;
public class CallProcedureWithInOutParam {
    public static void main(String[] args) {
        String sql = "{call add_numbers(?, ?, ?)}";
        try (Connection conn = DatabaseUtil.getConnection();
             CallableStatement cstmt = conn.prepareCall(sql)) {
            int num1 = 50;
            int num2 = 25;
            // 设置IN参数
            cstmt.setInt(1, num1);
            cstmt.setInt(2, num2);
            // 注册OUT参数
            cstmt.registerOutParameter(3, Types.INTEGER);
            // 执行
            cstmt.execute();
            // 获取OUT参数的值
            int sum = cstmt.getInt(3);
            System.out.println(num1 + " + " + num2 + " = " + sum);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

示例 4:返回结果集的存储过程 (最常用)

这是最复杂也最实用的场景,Oracle 使用 REF CURSOR 来返回结果集。

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
public class CallProcedureWithResultSet {
    public static void main(String[] args) {
        // 注意:这里需要指定参数的类型,Oracle中REF CURSOR的类型是OracleCursor
        // 或者使用标准的Types.REF_CURSOR (JDBC 3.0+)
        String sql = "{call get_employees_by_dept(?, ?)}";
        try (Connection conn = DatabaseUtil.getConnection();
             CallableStatement cstmt = conn.prepareCall(sql)) {
            int deptId = 50; // 查询部门ID为50的员工
            // 设置IN参数
            cstmt.setInt(1, deptId);
            // 注册OUT参数
            // 对于REF CURSOR,使用 Types.REF_CURSOR
            cstmt.registerOutParameter(2, Types.REF_CURSOR);
            // 执行
            // 对于返回结果集的存储过程,execute() 返回 false
            // 如果是 update/insert/delete,executeUpdate() 返回影响的行数
            boolean hasResults = cstmt.execute();
            // 获取结果集
            // 使用 cstmt.getObject() 来获取 REF CURSOR 对象,并将其转换为 ResultSet
            if (hasResults) {
                // 注意:在Oracle驱动中,通常可以直接通过getResultSet获取
                ResultSet rs = (ResultSet) cstmt.getObject(2);
                System.out.println("Employees in Department " + deptId + ":");
                System.out.println("---------------------------------");
                while (rs.next()) {
                    int empId = rs.getInt("employee_id");
                    String firstName = rs.getString("first_name");
                    String lastName = rs.getString("last_name");
                    System.out.println("ID: " + empId + ", Name: " + firstName + " " + lastName);
                }
                rs.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

最佳实践与注意事项

使用 try-with-resources

从 Java 7 开始,推荐使用 try-with-resources 语句来自动管理 Connection, Statement, ResultSet 等资源,它能确保这些资源在代码块执行完毕后自动关闭,即使发生异常也是如此。

// 推荐
try (Connection conn = DatabaseUtil.getConnection();
     CallableStatement cstmt = conn.prepareCall(sql)) {
    // ... your code
} catch (SQLException e) {
    // ...
}

使用 PreparedStatement

对于带参数的存储过程,CallableStatement 本身就是 PreparedStatement 的子类,它已经内置了防止 SQL 注入的机制。永远不要使用字符串拼接的方式来构建调用语句。

// 错误示例!有SQL注入风险
String empId = "101";
String sql = "{call get_employee_name(" + empId + ", ?)}";
// 正确示例
String sql = "{call get_employee_name(?, ?)}";
CallableStatement cstmt = conn.prepareCall(sql);
cstmt.setInt(1, 101); // 使用set方法设置参数

处理大数据类型 (CLOB, BLOB)

处理 CLOB (字符大对象) 和 BLOB (二进制大对象) 时,需要使用专门的 java.sql.Clobjava.sql.Blob 接口。

写入 CLOB 示例:

// 假设存储过程 p_update_clob(p_id IN NUMBER, p_clob IN CLOB)
cstmt.setInt(1, 123);
String largeText = "This is a very large text content...";
// 将字符串转换为Clob
java.sql.Clob clob = conn.createClob();
clob.setString(1, largeText);
cstmt.setClob(2, clob);

读取 CLOB 示例:

// 假设存储过程 p_get_clob(p_id IN NUMBER, p_clob OUT CLOB)
cstmt.registerOutParameter(2, Types.CLOB);
cstmt.execute();
java.sql.Clob clob = cstmt.getClob(2);
String content = clob.getSubString(1, (int) clob.length());

事务管理

默认情况下,JDBC 的 Connection 是在自动提交模式下的,在调用多个存储过程或执行多个SQL语句时,如果它们属于同一个业务逻辑单元,应该手动管理事务。

Connection conn = null;
try {
    conn = DatabaseUtil.getConnection();
    conn.setAutoCommit(false); // 关闭自动提交
    // 调用存储过程1
    CallableStatement cstmt1 = conn.prepareCall("{call procedure1(?)}");
    // ... 设置参数并执行
    cstmt1.execute();
    // 调用存储过程2
    CallableStatement cstmt2 = conn.prepareCall("{call procedure2(?)}");
    // ... 设置参数并执行
    cstmt2.execute();
    conn.commit(); // 所有操作成功,提交事务
    System.out.println("Transaction committed successfully.");
} catch (SQLException e) {
    if (conn != null) {
        try {
            conn.rollback(); // 发生异常,回滚事务
            System.out.println("Transaction rolled back.");
        } catch (SQLException ex) {
            ex.printStackTrace();
        }
    }
    e.printStackTrace();
} finally {
    if (conn != null) {
        try {
            conn.setAutoCommit(true); // 恢复自动提交模式
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

希望这份详细的指南能帮助你掌握在 Java 中调用 Oracle 存储过程的全部要点!

分享:
扫描分享到社交APP
上一篇
下一篇