杰瑞科技汇

Java如何调用Oracle存储过程?

  1. 准备工作: 创建一个测试用的 Oracle 存储过程。
  2. 核心概念: JDBC 调用存储过程的关键对象 (CallableStatement)。
  3. 完整代码示例: 分别演示调用无参数、有 IN 参数、有 OUT 参数以及有 IN OUT 参数的存储过程。
  4. 最佳实践与注意事项: 包括资源管理、异常处理和 SQL 注入防范。

准备工作:创建一个测试用的 Oracle 存储过程

为了演示,我们在 Oracle 数据库中创建几个不同类型的存储过程,假设你有一个用户表 T_USER,结构如下:

Java如何调用Oracle存储过程?-图1
(图片来源网络,侵删)
CREATE TABLE T_USER (
    ID          NUMBER PRIMARY KEY,
    NAME        VARCHAR2(50) NOT NULL,
    EMAIL       VARCHAR2(100),
    CREATE_DATE DATE
);

我们为这个表创建几个存储过程:

示例 1: 无参数存储过程 查询并打印所有用户信息(仅用于演示,实际开发中存储过程通常会返回结果集或值)。

CREATE OR REPLACE PROCEDURE P_LIST_USERS AS
BEGIN
  FOR user_rec IN (SELECT * FROM T_USER) LOOP
    DBMS_OUTPUT.PUT_LINE('ID: ' || user_rec.id || ', Name: ' || user_rec.name);
  END LOOP;
END P_LIST_USERS;
/

示例 2: 带 IN 参数的存储过程 根据用户 ID 查询用户信息。

CREATE OR REPLACE PROCEDURE P_GET_USER_BY_ID(p_id IN NUMBER, p_name OUT VARCHAR2, p_email OUT VARCHAR2) AS
BEGIN
  SELECT NAME, EMAIL INTO p_name, p_email FROM T_USER WHERE ID = p_id;
EXCEPTION
  WHEN NO_DATA_FOUND THEN
    p_name := NULL;
    p_email := NULL;
END P_GET_USER_BY_ID;
/

示例 3: 带 IN OUT 参数的存储过程 给用户名添加一个前缀,并返回新的用户名。

Java如何调用Oracle存储过程?-图2
(图片来源网络,侵删)
CREATE OR REPLACE PROCEDURE P_UPDATE_USER_NAME(p_id IN NUMBER, p_name IN OUT VARCHAR2) AS
BEGIN
  UPDATE T_USER SET NAME = 'Updated_' || p_name WHERE ID = p_id;
  -- 将 IN OUT 参数也更新为新的值,返回给调用者
  p_name := 'Updated_' || p_name;
END P_UPDATE_USER_NAME;
/

核心概念:CallableStatement

在 JDBC 中,java.sql.CallableStatement 接口专门用于执行 SQL 存储过程或函数,它继承自 PreparedStatement,因此也具有预编译 SQL 语句以防止 SQL 注入的优点。

调用存储过程的语法是 {call procedure_name(?, ?, ...)},对于函数,语法是 {? = call function_name(?, ?, ...)}

CallableStatement 使用 registerOutParameter(int paramIndex, int sqlType) 方法来注册一个 OUT 或 IN OUT 参数。sqlType 是 JDBC 的类型常量,如 Types.VARCHAR, Types.INTEGER, Types.DATE 等。


完整代码示例

确保你的项目中包含了 Oracle JDBC 驱动(ojdbc.jar 或 ojdbcX.jar),如果你使用 Maven,可以添加以下依赖:

Java如何调用Oracle存储过程?-图3
(图片来源网络,侵删)
<dependency>
    <groupId>com.oracle.database.jdbc</groupId>
    <artifactId>ojdbc8</artifactId>
    <version>19.3.0.0</version> <!-- 使用适合你数据库版本的版本 -->
</dependency>

下面是完整的 Java 调用示例:

import java.sql.*;
import java.util.Properties;
public class OracleStoredProcedureExample {
    // 数据库连接信息
    private static final String DB_URL = "jdbc:oracle:thin:@localhost:1521:ORCLPDB1";
    private static final String DB_USER = "your_username";
    private static final String DB_PASSWORD = "your_password";
    public static void main(String[] args) {
        // 为了演示,先初始化一些数据
        initializeTestData();
        // 使用 try-with-resources 确保资源自动关闭
        try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD)) {
            System.out.println("成功连接到 Oracle 数据库!");
            // --- 示例 1: 调用无参数的存储过程 ---
            callProcedureWithoutParams(conn);
            // --- 示例 2: 调用带 IN 和 OUT 参数的存储过程 ---
            callProcedureWithInOutParams(conn);
            // --- 示例 3: 调用带 IN OUT 参数的存储过程 ---
            callProcedureWithInOutParam(conn);
        } catch (SQLException e) {
            System.err.println("数据库操作出错: " + e.getMessage());
            e.printStackTrace();
        }
    }
    /**
     * 示例 1: 调用无参数的存储过程 P_LIST_USERS
     */
    private static void callProcedureWithoutParams(Connection conn) throws SQLException {
        System.out.println("\n--- 调用无参数存储过程 P_LIST_USERS ---");
        String sql = "{call P_LIST_USERS}";
        try (CallableStatement cstmt = conn.prepareCall(sql)) {
            // 执行存储过程
            cstmt.execute();
            // 注意:这个存储过程只是用 DBMS_OUTPUT 打印,Java 端无法直接获取。
            // 要从存储过程获取结果集,需要使用 Oracle 的 REFCURSOR 类型。
            System.out.println("P_LIST_USERS 执行完毕,请查看数据库的 DBMS_OUTPUT。");
        }
    }
    /**
     * 示例 2: 调用带 IN 和 OUT 参数的存储过程 P_GET_USER_BY_ID
     */
    private static void callProcedureWithInOutParams(Connection conn) throws SQLException {
        System.out.println("\n--- 调用带 IN/OUT 参数的存储过程 P_GET_USER_BY_ID ---");
        int userIdToFind = 1;
        String userName = null;
        String userEmail = null;
        String sql = "{call P_GET_USER_BY_ID(?, ?, ?)}";
        try (CallableStatement cstmt = conn.prepareCall(sql)) {
            // 设置 IN 参数
            cstmt.setInt(1, userIdToFind);
            // 注册 OUT 参数 (索引从 1 开始)
            // 第二个参数是 p_name (VARCHAR2)
            cstmt.registerOutParameter(2, Types.VARCHAR);
            // 第三个参数是 p_email (VARCHAR2)
            cstmt.registerOutParameter(3, Types.VARCHAR);
            // 执行存储过程
            cstmt.execute();
            // 获取 OUT 参数的值
            userName = cstmt.getString(2);
            userEmail = cstmt.getString(3);
            System.out.println("查询结果: ID=" + userIdToFind + ", Name=" + userName + ", Email=" + userEmail);
        }
    }
    /**
     * 示例 3: 调用带 IN OUT 参数的存储过程 P_UPDATE_USER_NAME
     */
    private static void callProcedureWithInOutParam(Connection conn) throws SQLException {
        System.out.println("\n--- 调用带 IN OUT 参数的存储过程 P_UPDATE_USER_NAME ---");
        int userIdToUpdate = 2;
        String originalName = "John Doe";
        String sql = "{call P_UPDATE_USER_NAME(?, ?)}";
        try (CallableStatement cstmt = conn.prepareCall(sql)) {
            // 设置 IN 参数
            cstmt.setInt(1, userIdToUpdate);
            // IN OUT 参数需要先设置它的初始值
            cstmt.setString(2, originalName);
            // 注册 IN OUT 参数
            cstmt.registerOutParameter(2, Types.VARCHAR);
            System.out.println("更新前,传递给存储过程的用户名: " + originalName);
            // 执行存储过程
            cstmt.execute();
            // 获取 IN OUT 参数返回的值
            String updatedName = cstmt.getString(2);
            System.out.println("存储过程返回的用户名: " + updatedName);
            System.out.println("请检查数据库中 ID=" + userIdToUpdate + " 的用户名是否已更新。");
        }
    }
    /**
     * 初始化测试数据
     */
    private static void initializeTestData() {
        // 这里省略了具体的初始化代码,实际使用时需要先插入数据
        System.out.println("假设已初始化测试数据到 T_USER 表。");
        // 
        // INSERT INTO T_USER (ID, NAME, EMAIL, CREATE_DATE) VALUES (1, 'Alice', 'alice@example.com', SYSDATE);
        // INSERT INTO T_USER (ID, NAME, EMAIL, CREATE_DATE) VALUES (2, 'John Doe', 'john@example.com', SYSDATE);
    }
}

最佳实践与注意事项

资源管理

数据库连接 (Connection)、语句 (Statement, PreparedStatement, CallableStatement) 和结果集 (ResultSet) 都是宝贵的资源,必须在使用后关闭。 最佳实践是使用 try-with-resources 语句,它能确保这些资源在代码块执行完毕后自动关闭,即使在发生异常时也是如此。

try (Connection conn = DriverManager.getConnection(...);
     CallableStatement cstmt = conn.prepareCall(...)) {
    // 执行逻辑
} // conn 和 cstmt 在这里会自动关闭

异常处理

数据库操作可能会抛出 SQLException,应该妥善处理这些异常,记录日志,并向用户友好的提示,不要简单地 printStackTrace()

防范 SQL 注入

CallableStatementPreparedStatement 一样,会预编译 SQL 语句,你传递的参数会被视为数据而不是 SQL 代码的一部分,从而有效防止 SQL 注入攻击。永远不要使用字符串拼接的方式来构建调用存储过程的 SQL

获取存储过程返回的结果集 (REFCURSOR)

上面的示例 1 中,存储过程使用 DBMS_OUTPUT,这在 Java 中无法直接获取,更常见的做法是让存储过程返回一个结果集,在 Oracle 中,这通过 REF CURSOR 实现。

Oracle 存储过程 (返回 REFCURSOR):

CREATE OR REPLACE PROCEDURE P_GET_USERS_RS(p_cursor OUT SYS_REFCURSOR) AS
BEGIN
  OPEN p_cursor FOR SELECT ID, NAME, EMAIL FROM T_USER;
END P_GET_USERS_RS;
/

Java 调用代码 (处理 REFCURSOR):

private static void callProcedureWithRefCursor(Connection conn) throws SQLException {
    System.out.println("\n--- 调用返回 REFCURSOR 的存储过程 ---");
    String sql = "{call P_GET_USERS_RS(?)}";
    try (CallableStatement cstmt = conn.prepareCall(sql)) {
        // 注册 OUT 参数,类型为 OracleTypes.CURSOR
        cstmt.registerOutParameter(1, OracleTypes.CURSOR);
        cstmt.execute();
        // 通过 get getObject() 获取结果集
        ResultSet rs = (ResultSet) cstmt.getObject(1);
        System.out.println("ID\tName\tEmail");
        System.out.println("----------------------");
        while (rs.next()) {
            System.out.println(rs.getInt("ID") + "\t" + rs.getString("NAME") + "\t" + rs.getString("EMAIL"));
        }
        rs.close(); // 关闭结果集
    }
}

注意:要使用 OracleTypes.CURSOR,你需要导入 oracle.jdbc.OracleTypes

性能考虑

存储过程通常用于将复杂的业务逻辑或数据处理放在数据库服务器端执行,这样可以减少网络数据传输,并利用数据库的优化能力,从而提高性能,也要注意不要滥用,将所有逻辑都放在存储过程可能会导致应用程序和数据库层之间的职责不清,难以维护和测试。

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