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

第一步:在 Oracle 中创建存储过程
为了演示,我们创建几个不同类型的存储过程,包括:
- 无参数、无返回值
- 有 IN 参数
- 有 OUT 参数
- 有 IN OUT 参数
- 返回结果集(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更新员工薪水。

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 中调用存储过程
环境准备
- JDBC 驱动: 下载 Oracle JDBC 驱动(如
ojdbc8.jar或ojdbc11.jar),并将其添加到你的项目中。 - 数据库连接: 确保你的 Java 应用可以连接到 Oracle 数据库。
使用原生 JDBC
这是最基础的方式,有助于理解底层原理。

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