跳转至

PL/SQL 与 Java / Spring 的协作模式

本章系统讲解 PL/SQL 与 Java / Spring 在真实项目中的协作方式。这是银行、金融、核心系统中最常见、也是最容易出问题的交互边界。掌握这一章,意味着你不仅会写 PL/SQL,也真正理解 “数据库逻辑 + 应用逻辑如何分工”


1. 为什么需要 PL/SQL + Java 协作

在企业级系统中,几乎不会出现:

❌ 全部逻辑写在 Java ❌ 全部逻辑写在 PL/SQL

而是 职责分离

层级 主要职责
Java / Spring 接口、流程编排、事务协调
PL/SQL 数据密集型业务、校验、批处理

2. 常见协作架构模式

2.1 经典三层结构

Controller
  ↓
Service(@Transactional)
  ↓
DAO / Mapper
  ↓
PL/SQL PACKAGE
  ↓
TABLE

📌 重点:

  • Java 不直接操作复杂 SQL
  • 所有核心数据逻辑收敛到 PACKAGE

3. Java 调用 PL/SQL 的三种方式(含完整执行示例)

下面以一个 账户转账 PACKAGE:pkg_account.p_transfer 为例,贯穿说明 Java / Spring 的调用与执行流程。

示例 PACKAGE(数据库侧)

create or replace package pkg_account is
  procedure p_transfer(
    p_from_id in number,
    p_to_id   in number,
    p_amount  in number,
    o_code    out number,
    o_msg     out varchar2
  );
end pkg_account;
/
create or replace package body pkg_account is
  procedure p_transfer(
    p_from_id in number,
    p_to_id   in number,
    p_amount  in number,
    o_code    out number,
    o_msg     out varchar2
  ) is
  begin
    update account set balance = balance - p_amount where id = p_from_id;
    update account set balance = balance + p_amount where id = p_to_id;

    o_code := 0;
    o_msg  := 'SUCCESS';
  exception
    when others then
      o_code := -1;
      o_msg  := sqlerrm;
  end;
end pkg_account;
/

3.1 JDBC CallableStatement(最底层、最清晰)

CallableStatement cs = conn.prepareCall(
  "{call pkg_account.p_transfer(?,?,?,?,?)}"
);
cs.setLong(1, fromId);
cs.setLong(2, toId);
cs.setBigDecimal(3, amount);
cs.registerOutParameter(4, Types.INTEGER);
cs.registerOutParameter(5, Types.VARCHAR);

cs.execute();

int code = cs.getInt(4);
String msg = cs.getString(5);

适合场景:

  • 原生 JDBC
  • 对执行细节、性能、事务边界要求高

3.2 Spring JdbcTemplate 调用示例

jdbcTemplate.execute(
  "{call pkg_account.p_transfer(?,?,?,?,?)}",
  (CallableStatement cs) -> {
    cs.setLong(1, fromId);
    cs.setLong(2, toId);
    cs.setBigDecimal(3, amount);
    cs.registerOutParameter(4, Types.INTEGER);
    cs.registerOutParameter(5, Types.VARCHAR);

    cs.execute();

    int code = cs.getInt(4);
    String msg = cs.getString(5);
    if (code != 0) {
      throw new RuntimeException(msg);
    }
    return null;
  }
);

3.3 MyBatis 调用存储过程(现场最常见)

Mapper XML

<select id="transfer" statementType="CALLABLE">
  {call pkg_account.p_transfer(
    #{fromId, mode=IN},
    #{toId, mode=IN},
    #{amount, mode=IN},
    #{code, mode=OUT, jdbcType=INTEGER},
    #{msg,  mode=OUT, jdbcType=VARCHAR}
  )}
</select>

Java 调用

Map<String, Object> param = new HashMap<>();
param.put("fromId", fromId);
param.put("toId", toId);
param.put("amount", amount);

mapper.transfer(param);

Integer code = (Integer) param.get("code");
String msg = (String) param.get("msg");

3.2 Spring JdbcTemplate

jdbcTemplate.execute(
  "call pkg_account.p_transfer(?,?,?)",
  (CallableStatement cs) -> {
    cs.setLong(1, fromId);
    cs.setLong(2, toId);
    cs.setBigDecimal(3, amount);
    return cs.execute();
  }
);

3.3 MyBatis 调用存储过程(常用)

<select id="transfer" statementType="CALLABLE">
  {call pkg_account.p_transfer(
    #{fromId, mode=IN},
    #{toId, mode=IN},
    #{amount, mode=IN}
  )}
</select>

📌 银行 / 项目现场 最常见


4. 入参 / 出参设计规范(非常重要)

4.1 推荐参数风格

procedure p_transfer(
  p_from_id in number,
  p_to_id   in number,
  p_amount  in number,
  o_code    out number,
  o_msg     out varchar2
);

📌 原则:

  • 不直接抛 DB 异常给 Java
  • 用返回码 + 消息

5. Java 与 PL/SQL 的事务边界(重点,含执行示例)

5.1 在线事务(Web 接口)——Java 控制事务

@Service
public class TransferService {

  @Transactional
  public void transfer(Long fromId, Long toId, BigDecimal amount) {
    mapper.transfer(Map.of(
      "fromId", fromId,
      "toId", toId,
      "amount", amount
    ));

    // 这里可以继续调用其他 PACKAGE
    // 最终由 Spring 统一 COMMIT / ROLLBACK
  }
}

PL/SQL 侧原则:

  • ❌ 不写 COMMIT
  • ❌ 不写 ROLLBACK

5.2 批处理事务——PL/SQL 控制事务

procedure p_batch_main is
begin
  for r in (select * from t_job where status = 'INIT') loop
    update t_job set status = 'DONE' where id = r.id;

    if mod(r.id, 1000) = 0 then
      commit;
    end if;
  end loop;

  commit;
end;

适用:

  • 夜间批处理
  • DBMS_SCHEDULER 作业

5.2 什么时候 PL/SQL 可以 COMMIT

场景 是否允许
Java 在线事务
夜间批处理
自治事务日志

6. 异常与错误码协作模式

6.1 PL/SQL 侧

exception
  when others then
    o_code := -1;
    o_msg  := sqlerrm;

6.2 Java 侧

if (code != 0) {
  throw new BusinessException(msg);
}

📌 避免:

  • Java 捕获 ORA-xxxxx

7. 批处理中的协作方式(执行视角)

7.1 Java 触发,数据库执行(常见)

// Java 只负责触发
schedulerClient.run("pkg_batch.main");
-- DB 中执行核心逻辑
begin
  pkg_batch.main;
end;
/

特点:

  • Java 无业务循环
  • 数据处理全部在 DB 内完成

7.2 数据库定时,Java 监控

  • DBMS_SCHEDULER 定时执行
  • Java 查询 batch_log 表
  • 展示运行状态 / 告警

8. 性能与连接管理注意点

  • PL/SQL 批处理尽量减少 Java 循环调用
  • 避免 Java for 循环中调用存储过程
  • 连接池大小与批处理时间协调

9. 面试高频问题(标准答案方向)

  • 为什么不用 Java 写所有逻辑?
  • 存储过程里能不能 COMMIT?
  • Java 和 PL/SQL 谁控制事务?

关键词:

职责分离、事务统一、接口稳定


10. 本章小结(工程师版)

  • Java 负责流程与事务
  • PL/SQL 负责数据密集型逻辑
  • 核心逻辑必须封装为 PACKAGE
  • 事务边界必须清晰

👉 下一章建议:PL/SQL 安全、权限与部署规范