杰瑞科技汇

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

我们将分步进行,涵盖从创建存储过程到使用不同 Java 方式(JDBC 和 JdbcTemplate)调用的完整流程。

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

第一步:在 Oracle 中创建存储过程

为了演示,我们创建几个不同类型的存储过程,包括:

  1. 无参数、无返回值
  2. 有 IN 参数
  3. 有 OUT 参数
  4. 有 IN OUT 参数
  5. 返回结果集(Ref Cursor)

假设我们有一个 EMPLOYEES 表:

CREATE TABLE EMPLOYEES (
    ID          NUMBER PRIMARY KEY,
    NAME        VARCHAR2(100),
    SALARY      NUMBER(10, 2),
    DEPARTMENT  VARCHAR2(50)
);
-- 插入一些测试数据
INSERT INTO EMPLOYEES (ID, NAME, SALARY, DEPARTMENT) VALUES (1, 'Alice', 5000, 'IT');
INSERT INTO EMPLOYEES (ID, NAME, SALARY, DEPARTMENT) VALUES (2, 'Bob', 6000, 'HR');
INSERT INTO EMPLOYEES (ID, NAME, SALARY, DEPARTMENT) VALUES (3, 'Charlie', 4500, 'IT');
COMMIT;

无参数、无返回值的存储过程

这个存储过程会简单地打印一条信息。

CREATE OR REPLACE PROCEDURE say_hello_proc AS
BEGIN
    DBMS_OUTPUT.PUT_LINE('Hello from Oracle Stored Procedure!');
END say_hello_proc;
/

有 IN 参数的存储过程

这个存储过程根据员工ID更新员工薪水。

Oracle存储过程如何与Java交互?-图2
(图片来源网络,侵删)
CREATE OR REPLACE PROCEDURE update_employee_salary_proc (
    p_emp_id IN NUMBER,
    p_new_salary IN NUMBER
) AS
BEGIN
    UPDATE EMPLOYEES
    SET SALARY = p_new_salary
    WHERE ID = p_emp_id;
    COMMIT; -- 记得提交事务
    DBMS_OUTPUT.PUT_LINE('Employee ' || p_emp_id || ' salary updated to ' || p_new_salary);
END update_employee_salary_proc;
/

有 OUT 参数的存储过程

这个存储过程根据员工ID查询员工姓名,并通过 OUT 参数返回。

CREATE OR REPLACE PROCEDURE get_employee_name_proc (
    p_emp_id IN NUMBER,
    p_emp_name OUT VARCHAR2
) AS
BEGIN
    SELECT NAME INTO p_emp_name
    FROM EMPLOYEES
    WHERE ID = p_emp_id;
END get_employee_name_proc;
/

有 IN OUT 参数的存储过程

这个存储过程给员工涨薪,并通过 IN OUT 参数返回新的薪水。

CREATE OR REPLACE PROCEDURE give_raise_proc (
    p_emp_id IN NUMBER,
    p_raise_amount IN OUT NUMBER
) AS
    v_current_salary NUMBER;
BEGIN
    -- 先获取当前薪水
    SELECT SALARY INTO v_current_salary
    FROM EMPLOYEES
    WHERE ID = p_emp_id;
    -- 计算新薪水
    p_raise_amount := v_current_salary + p_raise_amount;
    -- 更新数据库
    UPDATE EMPLOYEES
    SET SALARY = p_raise_amount
    WHERE ID = p_emp_id;
    COMMIT;
    DBMS_OUTPUT.PUT_LINE('Employee ' || p_emp_id || ' new salary is ' || p_raise_amount);
END give_raise_proc;
/

返回结果集(使用 Ref Cursor)

这是最常用也最复杂的一种,用于返回多行数据。

CREATE OR REPLACE PACKAGE emp_data_pkg AS
    -- 定义一个 Ref Cursor 类型
    TYPE emp_cursor_type IS REF CURSOR;
END emp_data_pkg;
/
CREATE OR REPLACE PROCEDURE get_employees_by_dept_proc (
    p_dept_name IN VARCHAR2,
    p_emp_cursor OUT emp_data_pkg.emp_cursor_type
) AS
BEGIN
    -- 打开游标,将查询结果关联到游标变量
    OPEN p_emp_cursor FOR
        SELECT ID, NAME, SALARY
        FROM EMPLOYEES
        WHERE DEPARTMENT = p_dept_name;
END get_employees_by_dept_proc;
/

第二步:在 Java 中调用存储过程

环境准备

  1. JDBC 驱动: 下载 Oracle JDBC 驱动(如 ojdbc8.jarojdbc11.jar),并将其添加到你的项目中。
  2. 数据库连接: 确保你的 Java 应用可以连接到 Oracle 数据库。

使用原生 JDBC

这是最基础的方式,有助于理解底层原理。

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

调用 say_hello_proc (无参数)

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.CallableStatement;
import java.sql.SQLException;
public class JdbcStoredProcedureExample {
    private static final String DB_URL = "jdbc:oracle:thin:@//your_host:your_port/your_service_name";
    private static final String USER = "your_username";
    private static final String PASS = "your_password";
    public static void main(String[] args) {
        // 注册驱动 (对于新版本JDBC驱动,通常可以省略)
        try {
            Class.forName("oracle.jdbc.OracleDriver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            return;
        }
        // 使用 try-with-resources 确保连接和语句被关闭
        try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS)) {
            if (conn != null) {
                System.out.println("Connected to the database!");
                // 1. 调用无参数、无返回值的存储过程
                callSimpleProcedure(conn);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    public static void callSimpleProcedure(Connection conn) throws SQLException {
        // 语法: {call procedure_name()}
        String sql = "{call say_hello_proc()}";
        try (CallableStatement cstmt = conn.prepareCall(sql)) {
            // 执行
            cstmt.execute();
            System.out.println("Called say_hello_proc successfully.");
        }
    }
}

调用 update_employee_salary_proc (IN 参数)

// 在 JdbcStoredProcedureExample 类中添加
public static void callProcedureWithInParam(Connection conn) throws SQLException {
    // 语法: {call procedure_name(?, ?)} ? 占位符
    String sql = "{call update_employee_salary_proc(?, ?)}";
    try (CallableStatement cstmt = conn.prepareCall(sql)) {
        // 设置 IN 参数
        cstmt.setInt(1, 1); // 第一个 ? 设置为员工ID 1
        cstmt.setDouble(2, 5500.00); // 第二个 ? 设置为新薪水 5500.00
        // 执行
        cstmt.executeUpdate();
        System.out.println("Called update_employee_salary_proc successfully.");
    }
}

调用 get_employee_name_proc (OUT 参数)

// 在 JdbcStoredProcedureExample 类中添加
public static void callProcedureWithOutParam(Connection conn) throws SQLException {
    // 语法: {call procedure_name(?, ?)} 第二个 ? 是 OUT 参数
    String sql = "{call get_employee_name_proc(?, ?)}";
    try (CallableStatement cstmt = conn.prepareCall(sql)) {
        // 设置 IN 参数
        cstmt.setInt(1, 2); // 查询ID为2的员工
        // 注册 OUT 参数 (必须指定类型)
        // Types.VARCHAR 对应 Oracle 的 VARCHAR2
        cstmt.registerOutParameter(2, java.sql.Types.VARCHAR);
        // 执行
        cstmt.execute();
        // 从 OUT 参数中获取值
        String empName = cstmt.getString(2);
        System.out.println("Employee name (from OUT param): " + empName);
    }
}

调用 give_raise_proc (IN OUT 参数)

// 在 JdbcStoredProcedureExample 类中添加
public static void callProcedureWithInOutParam(Connection conn) throws SQLException {
    String sql = "{call give_raise_proc(?, ?)}";
    try (CallableStatement cstmt = conn.prepareCall(sql)) {
        // 设置 IN 参数
        cstmt.setInt(1, 3); // 给ID为3的员工涨薪
        // 注册 IN OUT 参数
        // 注意:这里传入的是要涨的金额,500
        cstmt.registerOutParameter(2, java.sql.Types.NUMERIC);
        // 设置 IN 值
        cstmt.setInt(2, 500); // 涨薪500
        // 执行
        cstmt.execute();
        // 获取 IN OUT 参数返回的最终薪水
        BigDecimal newSalary = cstmt.getBigDecimal(2);
        System.out.println("Employee new salary (from INOUT param): " + newSalary);
    }
}

调用 get_employees_by_dept_proc (Ref Cursor)

这是最关键的,Oracle 使用 REF CURSOR 来返回结果集,在 JDBC 中它被映射为 OracleResultSet

import oracle.jdbc.OracleCallableStatement;
import oracle.jdbc.OracleResultSet;
import java.math.BigDecimal;
// 在 JdbcStoredProcedureExample 类中添加
public static void callProcedureWithRefCursor(Connection conn) throws SQLException {
    // 语法: {call package_name.procedure_name(?, ?)}
    String sql = "{call emp_data_pkg.get_employees_by_dept_proc(?, ?)}";
    try (CallableStatement cstmt = conn.prepareCall(sql)) {
        // 设置 IN 参数
        cstmt.setString(1, "IT"); // 查询IT部门的员工
        // 注册 OUT 参数
        // OracleRefCursor 是 Oracle 特有的类型
        cstmt.registerOutParameter(2, oracle.jdbc.OracleTypes.CURSOR);
        // 执行
        cstmt.execute();
        // 从 OUT 参数中获取 Ref Cursor
        // 注意:必须将 cstmt 转换为 OracleCallableStatement 才能获取 OracleResultSet
        OracleCallableStatement ocstmt = (OracleCallableStatement) cstmt;
        OracleResultSet orset = (OracleResultSet) ocstmt.getCursor(2);
        System.out.println("Employees in IT department:");
        // 遍历结果集
        while (orset.next()) {
            int id = orset.getInt("ID");
            String name = orset.getString("NAME");
            BigDecimal salary = orset.getBigDecimal("SALARY");
            System.out.printf("ID: %d, Name: %s, Salary: %.2f%n", id, name, salary);
        }
        // 关闭游标
        orset.close();
    }
}

使用 Spring 的 JdbcTemplate

如果你的项目是基于 Spring 框架的,使用 JdbcTemplate 会更简洁、更安全。

Maven 依赖

<dependencies>
    <!-- Spring JDBC -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.3.23</version> <!-- 使用合适的版本 -->
    </dependency>
    <!-- Oracle JDBC Driver -->
    <dependency>
        <groupId>com.oracle.database.jdbc</groupId>
        <artifactId>ojdbc8</artifactId>
        <version>19.3.0.0</version> <!-- 使用合适的版本 -->
    </dependency>
</dependencies>

配置数据源和 JdbcTemplate

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
@Configuration
public class AppConfig {
    @Bean
    public DataSource dataSource() {
        // 简化示例,实际项目中建议使用连接池 (如 HikariCP)
        return new DriverManagerDataSource(
            "jdbc:oracle:thin:@//your_host:your_port/your_service_name",
            "your_username",
            "your_password"
        );
    }
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

使用 JdbcTemplate 调用存储过程

JdbcTemplate 提供了 call 方法来简化调用。

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.core.simple.SimpleJdbcCall;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.sql.Types;
import java.util.Map;
@Service
public class StoredProcedureService {
    private final JdbcTemplate jdbcTemplate;
    public StoredProcedureService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    // 调用带 OUT 参数的存储过程
    public String getEmployeeName(int empId) {
        // 使用 call 方法,需要手动构建参数和返回值映射
        Map<String, Object> result = jdbcTemplate.call(
            con -> con.prepareCall("{call get_employee_name_proc(?, ?)}"),
            // 定义参数列表
            List.of(
                new SqlParameter(Types.NUMERIC), // IN 参数
                new SqlOutParameter("emp_name", Types.VARCHAR) // OUT 参数
            ),
            // 设置 IN 参数值
            Map.of(1, empId)
        );
        // 从结果中获取 OUT 参数的值
        return (String) result.get("emp_name");
    }
    // 调用返回 Ref Cursor 的存储过程 (更推荐的方式)
    public List<Employee> getEmployeesByDept(String deptName) {
        SimpleJdbcCall jdbcCall = new SimpleJdbcCall(jdbcTemplate)
            .withSchemaName("YOUR_SCHEMA") // 可选,指定模式
            .withCatalogName("EMP_DATA_PKG") // 包名
            .withProcedureName("GET_EMPLOYEES_BY_DEPT_PROC") // 存储过程名
            .declareParameters(
                new SqlParameter("P_DEPT_NAME", Types.VARCHAR), // IN 参数
                new SqlOutParameter("P_EMP_CURSOR", // OUT 参数 (Ref Cursor)
                    oracle.jdbc.OracleTypes.CURSOR, // Oracle 特有的类型
                    // RowMapper 用于将每一行映射为 Java 对象
                    (rs, rowNum) -> new Employee(
                        rs.getInt("ID"),
                        rs.getString("NAME"),
                        rs.getBigDecimal("SALARY")
                    ))
            );
        // 执行并获取结果
        Map<String, Object> out = jdbcCall.execute(deptName);
        // 从结果中获取映射后的列表
        return (List<Employee>) out.get("P_EMP_CURSOR");
    }
}
// 辅助的 Employee 类
class Employee {
    private int id;
    private String name;
    private BigDecimal salary;
    // 构造器、Getter 和 Setter
    public Employee(int id, String name, BigDecimal salary) {
        this.id = id;
        this.name = name;
        this.salary = salary;
    }
    @Override
    public String toString() {
        return "Employee{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", salary=" + salary +
                '}';
    }
    // ... getter and setter
}

总结与最佳实践

特性 原生 JDBC Spring JdbcTemplate
易用性 复杂,需要处理大量样板代码(注册参数、关闭资源等) 简洁,自动管理资源,API 更友好
类型安全 弱,容易出错(如参数索引和类型) 强,通过 SqlParameter 等对象定义,编译时检查
Ref Cursor 麻烦,需要强制类型转换 (OracleCallableStatement) 简洁,SimpleJdbcCall + RowMapper 结合得很好
依赖 只需 JDBC 驱动 需要 Spring 框架
适用场景 小型项目、学习 JDBC 原理 大多数 Spring 项目,推荐使用

最佳实践建议:

  1. 优先使用 JdbcTemplate:在 Spring 项目中,JdbcTemplate(特别是 SimpleJdbcCall)是调用存储过程的首选,它能极大提升开发效率和代码可读性。
  2. 使用 try-with-resources:如果使用原生 JDBC,务必使用 try-with-resources 来确保 Connection, Statement, ResultSet 等资源被正确关闭,避免内存泄漏。
  3. 处理异常:妥善处理 SQLException,根据业务需要进行重试或记录日志。
  4. 避免 SQL 注入:虽然存储过程本身可以降低 SQL 注入风险,但在调用时,使用 PreparedStatementJdbcTemplate 内部默认使用)来设置参数,是防止注入的最佳实践。
  5. 明确事务边界:在调用更新类存储过程时,要明确事务的边界。JdbcTemplate 可以方便地与 Spring 的 @Transactional 注解集成。
分享:
扫描分享到社交APP
上一篇
下一篇