Oracle 19c入门学习教程,从入门到精通,Oracle 过程、函数、触发器和包详解(7)

Oracle 过程、函数、触发器和包详解

本章系统讲解 Oracle 数据库中 存储过程(Procedure)函数(Function)触发器(Trigger)程序包(Package) 的语法、使用方法及最佳实践。这些是 PL/SQL 高级编程的核心组件,广泛用于业务逻辑封装、数据完整性控制和模块化开发。


一、环境准备与安装说明

1. 前提条件

  • 已安装 Oracle Database (如 19c、21c 或免费的 Oracle XE
  • 已启用 SCOTT 用户(用于练习)

2. 启用 SCOTT 用户(若未启用)

sql 复制代码
-- 以 sysdba 登录
CONNECT / AS SYSDBA

-- 解锁并设置密码
ALTER USER scott ACCOUNT UNLOCK IDENTIFIED BY tiger;

-- 连接 scott
CONNECT scott/tiger

3. 开启输出(用于调试)

sql 复制代码
SET SERVEROUTPUT ON;

✅ 推荐工具:SQL*PlusOracle SQL Developer


二、存储过程(Stored Procedure)

1. 什么是存储过程?

  • 存储在数据库中的命名 PL/SQL 块
  • 可接受参数、执行 DML/DDL、返回结果(通过 OUT 参数)
  • 不返回值(与函数区别)
  • 可被应用程序或其它 PL/SQL 调用

2. 创建存储过程语法

plsql 复制代码
CREATE [OR REPLACE] PROCEDURE procedure_name (
  parameter1 [IN | OUT | IN OUT] datatype [DEFAULT value],
  parameter2 ...
)
IS
  -- 声明变量(可选)
BEGIN
  -- 执行逻辑
[EXCEPTION ...]
END [procedure_name];
/

3. 存储过程的参数模式

模式 说明
IN 默认模式,输入参数(只读)
OUT 输出参数(调用后返回值)
IN OUT 输入输出参数(可修改)

4. 示例:创建带参数的存储过程

案例1:根据员工编号查询姓名和工资(使用 OUT 参数)
plsql 复制代码
CREATE OR REPLACE PROCEDURE get_emp_info (
  p_empno   IN  emp.empno%TYPE,          -- 输入员工编号
  p_ename   OUT emp.ename%TYPE,          -- 输出姓名
  p_sal     OUT emp.sal%TYPE             -- 输出工资
)
IS
BEGIN
  SELECT ename, sal
  INTO p_ename, p_sal
  FROM emp
  WHERE empno = p_empno;

EXCEPTION
  WHEN NO_DATA_FOUND THEN
    p_ename := NULL;
    p_sal   := NULL;
    DBMS_OUTPUT.PUT_LINE('Error: Employee ' || p_empno || ' not found.');
  WHEN OTHERS THEN
    DBMS_OUTPUT.PUT_LINE('Unexpected error: ' || SQLERRM);
END get_emp_info;
/
调用存储过程(在匿名块中)
plsql 复制代码
DECLARE
  v_name emp.ename%type;
  v_sal  emp.sal%type;
BEGIN
  get_emp_info(7369, v_name, v_sal);  -- 调用过程
  DBMS_OUTPUT.PUT_LINE('Name: ' || v_name || ', Salary: ' || v_sal);
END;
/

5. IN 参数的默认值

plsql 复制代码
-- 创建带默认值的过程
CREATE OR REPLACE PROCEDURE raise_salary (
  p_empno   IN emp.empno%TYPE,
  p_percent IN NUMBER DEFAULT 10   -- 默认加薪 10%
)
IS
BEGIN
  UPDATE emp
  SET sal = sal * (1 + p_percent / 100)
  WHERE empno = p_empno;

  IF SQL%ROWCOUNT = 0 THEN
    RAISE_APPLICATION_ERROR(-20001, 'Employee not found');
  END IF;

  COMMIT;
  DBMS_OUTPUT.PUT_LINE('Salary raised by ' || p_percent || '% for employee ' || p_empno);
END raise_salary;
/
调用方式(两种)
plsql 复制代码
-- 使用默认值
EXEC raise_salary(7369);

-- 显式指定
EXEC raise_salary(7369, 15);

EXEC 是 SQL*Plus 快捷命令,等价于 BEGIN proc; END;

6. 删除存储过程

sql 复制代码
DROP PROCEDURE get_emp_info;

三、函数(Function)

1. 什么是函数?

  • 与存储过程类似,但 必须返回一个值
  • 可在 SQL 语句中直接调用(如 SELECT my_func() FROM dual
  • 不能执行 DML(除非使用自治事务)

2. 创建函数语法

plsql 复制代码
CREATE [OR REPLACE] FUNCTION function_name (
  parameter_list
) RETURN return_datatype
IS
  -- 声明
BEGIN
  -- 逻辑
  RETURN expression;
END;
/

3. 示例:计算员工年薪(含奖金)

plsql 复制代码
CREATE OR REPLACE FUNCTION calc_annual_salary (
  p_empno IN emp.empno%TYPE
) RETURN NUMBER
IS
  v_sal   emp.sal%TYPE;
  v_comm  emp.comm%TYPE;
BEGIN
  SELECT sal, NVL(comm, 0)
  INTO v_sal, v_comm
  FROM emp
  WHERE empno = p_empno;

  -- 年薪 = 月薪*12 + 奖金
  RETURN (v_sal * 12) + v_comm;
EXCEPTION
  WHEN NO_DATA_FOUND THEN
    RETURN NULL;
END calc_annual_salary;
/

4. 调用函数

方式1:在 SQL 中调用
sql 复制代码
SELECT ename, calc_annual_salary(empno) AS annual_sal
FROM emp
WHERE deptno = 20;
方式2:在 PL/SQL 中调用
plsql 复制代码
DECLARE
  v_annual NUMBER;
BEGIN
  v_annual := calc_annual_salary(7369);
  DBMS_OUTPUT.PUT_LINE('Annual salary: ' || v_annual);
END;
/

5. 删除函数

sql 复制代码
DROP FUNCTION calc_annual_salary;

⚠️ 注意:若函数被其他对象依赖,需先删除依赖项。


四、触发器(Trigger)

1. 触发器简介

  • 在特定 DML、DDL 或数据库事件 发生时自动执行的 PL/SQL 块
  • 用于实现 数据完整性、审计、日志、业务规则

2. 触发器类型

类型 触发时机 说明
语句级 每条 DML 语句触发一次 无法访问 :OLD/:NEW
行级 每行受影响时触发一次 可用 :OLD(旧值)、:NEW(新值)
替换触发器(INSTEAD OF) 用于视图 替代 DML 操作
用户事件触发器 LOGON、LOGOFF、DDL 等 用于审计

3. 语句级触发器示例

plsql 复制代码
-- 在 emp 表更新后记录操作时间
CREATE OR REPLACE TRIGGER trg_emp_update_stmt
  AFTER UPDATE ON emp
DECLARE
BEGIN
  DBMS_OUTPUT.PUT_LINE('Employee table updated at ' || TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'));
END;
/

此触发器对整个 UPDATE 语句触发一次。


4. 行级触发器示例(最常用)

plsql 复制代码
-- 禁止工资降低
CREATE OR REPLACE TRIGGER trg_emp_sal_check
  BEFORE UPDATE OF sal ON emp
  FOR EACH ROW  -- 关键:行级触发
BEGIN
  IF :NEW.sal < :OLD.sal THEN
    RAISE_APPLICATION_ERROR(-20002, 'Error: Salary cannot be decreased!');
  END IF;
END;
/

:OLD.sal 是更新前的值,:NEW.sal 是更新后的新值。

测试
sql 复制代码
UPDATE emp SET sal = 1000 WHERE empno = 7369;  -- 若原工资 >1000,将报错

5. 替换触发器(INSTEAD OF)

用于对 不可直接更新的视图 执行 DML。

sql 复制代码
-- 创建视图
CREATE VIEW emp_dept_view AS
SELECT e.empno, e.ename, d.dname
FROM emp e JOIN dept d ON e.deptno = d.deptno;

-- 创建 INSTEAD OF 触发器
CREATE OR REPLACE TRIGGER trg_instead_of_insert
  INSTEAD OF INSERT ON emp_dept_view
  FOR EACH ROW
DECLARE
  v_deptno dept.deptno%TYPE;
BEGIN
  -- 根据部门名查 deptno
  SELECT deptno INTO v_deptno
  FROM dept
  WHERE dname = :NEW.dname;

  -- 插入到 emp 表
  INSERT INTO emp (empno, ename, deptno)
  VALUES (:NEW.empno, :NEW.ename, v_deptno);
EXCEPTION
  WHEN NO_DATA_FOUND THEN
    RAISE_APPLICATION_ERROR(-20003, 'Department not found: ' || :NEW.dname);
END;
/
测试插入
sql 复制代码
INSERT INTO emp_dept_view (empno, ename, dname)
VALUES (8001, 'ALICE', 'SALES');

6. 用户事件触发器(审计登录)

plsql 复制代码
-- 记录用户登录信息(需在 sys 下创建)
CREATE TABLE login_audit (
  username VARCHAR2(30),
  logon_time DATE,
  ip_address VARCHAR2(40)
);

CREATE OR REPLACE TRIGGER trg_logon_audit
  AFTER LOGON ON DATABASE
BEGIN
  INSERT INTO login_audit (username, logon_time, ip_address)
  VALUES (USER, SYSDATE, SYS_CONTEXT('USERENV', 'IP_ADDRESS'));
END;
/

此触发器需 DBA 权限,通常用于安全审计。


7. 删除触发器

sql 复制代码
DROP TRIGGER trg_emp_sal_check;

五、程序包(Package)

1. 什么是程序包?

  • 规范(Specification):声明公共接口(过程、函数、变量)
  • 主体(Body):实现细节
  • 支持 封装、重载、全局变量、初始化块

2. 程序包的优势

  • 模块化设计
  • 提高性能(一次性加载)
  • 支持函数/过程重载
  • 隐藏实现细节

3. 创建程序包规范

plsql 复制代码
-- 包规范:定义公共接口
CREATE OR REPLACE PACKAGE emp_pkg IS
  -- 公共常量
  c_max_salary CONSTANT NUMBER := 10000;

  -- 过程声明
  PROCEDURE hire_employee (
    p_empno  IN emp.empno%TYPE,
    p_ename  IN emp.ename%TYPE,
    p_job    IN emp.job%TYPE,
    p_mgr    IN emp.mgr%TYPE DEFAULT NULL,
    p_hiredate IN DATE DEFAULT SYSDATE,
    p_sal    IN emp.sal%TYPE,
    p_comm   IN emp.comm%TYPE DEFAULT NULL,
    p_deptno IN emp.deptno%TYPE
  );

  -- 函数声明
  FUNCTION get_dept_name(p_deptno IN dept.deptno%TYPE) RETURN VARCHAR2;

  -- 重载函数:根据员工编号或姓名查工资
  FUNCTION get_salary(p_empno IN emp.empno%TYPE) RETURN NUMBER;
  FUNCTION get_salary(p_ename IN emp.ename%TYPE) RETURN NUMBER;

END emp_pkg;
/

4. 创建程序包主体

plsql 复制代码
CREATE OR REPLACE PACKAGE BODY emp_pkg IS

  -- 实现 hire_employee
  PROCEDURE hire_employee (
    p_empno IN emp.empno%TYPE,
    p_ename IN emp.ename%TYPE,
    p_job   IN emp.job%TYPE,
    p_mgr   IN emp.mgr%TYPE DEFAULT NULL,
    p_hiredate IN DATE DEFAULT SYSDATE,
    p_sal   IN emp.sal%TYPE,
    p_comm  IN emp.comm%TYPE DEFAULT NULL,
    p_deptno IN emp.deptno%TYPE
  ) IS
  BEGIN
    IF p_sal > c_max_salary THEN
      RAISE_APPLICATION_ERROR(-20004, 'Salary exceeds maximum allowed.');
    END IF;

    INSERT INTO emp (empno, ename, job, mgr, hiredate, sal, comm, deptno)
    VALUES (p_empno, p_ename, p_job, p_mgr, p_hiredate, p_sal, p_comm, p_deptno);
    COMMIT;
    DBMS_OUTPUT.PUT_LINE('Employee ' || p_ename || ' hired successfully.');
  END hire_employee;

  -- 实现 get_dept_name
  FUNCTION get_dept_name(p_deptno IN dept.deptno%TYPE) RETURN VARCHAR2 IS
    v_dname dept.dname%TYPE;
  BEGIN
    SELECT dname INTO v_dname FROM dept WHERE deptno = p_deptno;
    RETURN v_dname;
  EXCEPTION
    WHEN NO_DATA_FOUND THEN RETURN 'Unknown Dept';
  END get_dept_name;

  -- 重载函数1:按编号查工资
  FUNCTION get_salary(p_empno IN emp.empno%TYPE) RETURN NUMBER IS
    v_sal emp.sal%TYPE;
  BEGIN
    SELECT sal INTO v_sal FROM emp WHERE empno = p_empno;
    RETURN v_sal;
  END get_salary;

  -- 重载函数2:按姓名查工资
  FUNCTION get_salary(p_ename IN emp.ename%TYPE) RETURN NUMBER IS
    v_sal emp.sal%TYPE;
  BEGIN
    SELECT sal INTO v_sal FROM emp WHERE ename = UPPER(p_ename);
    RETURN v_sal;
  END get_salary;

  -- 初始化块(可选)
  BEGIN
    DBMS_OUTPUT.PUT_LINE('emp_pkg initialized at ' || SYSDATE);
  END emp_pkg;
/

5. 调用程序包中的成员

plsql 复制代码
-- 调用过程
BEGIN
  emp_pkg.hire_employee(
    p_empno => 8002,
    p_ename => 'BOB',
    p_job   => 'ANALYST',
    p_sal   => 4000,
    p_deptno=> 20
  );
END;
/

-- 调用函数
SELECT emp_pkg.get_dept_name(20) FROM dual;

DECLARE
  v_sal NUMBER;
BEGIN
  v_sal := emp_pkg.get_salary(7369);        -- 按编号
  DBMS_OUTPUT.PUT_LINE('Salary by ID: ' || v_sal);

  v_sal := emp_pkg.get_salary('KING');      -- 按姓名(重载)
  DBMS_OUTPUT.PUT_LINE('Salary by Name: ' || v_sal);
END;
/

6. 删除程序包

sql 复制代码
-- 先删主体,再删规范(或直接删规范)
DROP PACKAGE emp_pkg;

删除规范会自动删除主体。


六、综合性实战案例

案例:员工管理系统(含过程、函数、触发器、包)

需求

  1. 创建包 hr_mgmt 管理员工
  2. 提供入职、加薪、查询功能
  3. 自动记录工资变更日志
  4. 禁止非法操作(如工资为负)
步骤1:创建日志表
sql 复制代码
CREATE TABLE salary_change_log (
  log_id     NUMBER GENERATED ALWAYS AS IDENTITY,
  empno      NUMBER(4),
  old_sal    NUMBER(7,2),
  new_sal    NUMBER(7,2),
  changed_by VARCHAR2(30) DEFAULT USER,
  change_ts  DATE DEFAULT SYSDATE
);
步骤2:创建触发器(自动记录工资变更)
plsql 复制代码
CREATE OR REPLACE TRIGGER trg_sal_change_log
  AFTER UPDATE OF sal ON emp
  FOR EACH ROW
BEGIN
  IF :OLD.sal != :NEW.sal THEN
    INSERT INTO salary_change_log (empno, old_sal, new_sal)
    VALUES (:OLD.empno, :OLD.sal, :NEW.sal);
  END IF;
END;
/
步骤3:创建程序包规范
plsql 复制代码
CREATE OR REPLACE PACKAGE hr_mgmt IS
  PROCEDURE add_employee(
    p_empno NUMBER, p_ename VARCHAR2, p_job VARCHAR2,
    p_sal NUMBER, p_deptno NUMBER
  );
  PROCEDURE give_raise(p_empno NUMBER, p_amount NUMBER);
  FUNCTION get_employee_info(p_empno NUMBER) RETURN VARCHAR2;
END hr_mgmt;
/
步骤4:创建程序包主体
plsql 复制代码
CREATE OR REPLACE PACKAGE BODY hr_mgmt IS

  PROCEDURE add_employee(
    p_empno NUMBER, p_ename VARCHAR2, p_job VARCHAR2,
    p_sal NUMBER, p_deptno NUMBER
  ) IS
  BEGIN
    IF p_sal <= 0 THEN
      RAISE_APPLICATION_ERROR(-20005, 'Salary must be positive.');
    END IF;
    INSERT INTO emp (empno, ename, job, sal, deptno, hiredate)
    VALUES (p_empno, UPPER(p_ename), UPPER(p_job), p_sal, p_deptno, SYSDATE);
    COMMIT;
  END;

  PROCEDURE give_raise(p_empno NUMBER, p_amount NUMBER) IS
    v_current_sal emp.sal%TYPE;
  BEGIN
    SELECT sal INTO v_current_sal FROM emp WHERE empno = p_empno;
    IF v_current_sal + p_amount > 20000 THEN
      RAISE_APPLICATION_ERROR(-20006, 'New salary exceeds limit.');
    END IF;
    UPDATE emp SET sal = sal + p_amount WHERE empno = p_empno;
    COMMIT;
  END;

  FUNCTION get_employee_info(p_empno NUMBER) RETURN VARCHAR2 IS
    v_info VARCHAR2(200);
  BEGIN
    SELECT ename || ' (' || job || ') - $' || sal
    INTO v_info
    FROM emp
    WHERE empno = p_empno;
    RETURN v_info;
  EXCEPTION
    WHEN NO_DATA_FOUND THEN RETURN 'Employee not found';
  END;

END hr_mgmt;
/
步骤5:测试
plsql 复制代码
-- 添加员工
EXEC hr_mgmt.add_employee(8003, 'Carol', 'Manager', 6000, 10);

-- 加薪
EXEC hr_mgmt.give_raise(8003, 500);

-- 查询
DECLARE
  v_info VARCHAR2(200);
BEGIN
  v_info := hr_mgmt.get_employee_info(8003);
  DBMS_OUTPUT.PUT_LINE(v_info);
END;
/

-- 查看日志
SELECT * FROM salary_change_log;

七、总结

组件 特点 使用场景
存储过程 无返回值,可执行 DML 业务操作(如批量处理)
函数 有返回值,可在 SQL 中调用 计算、转换
触发器 自动执行 审计、约束、日志
程序包 封装多个过程/函数 模块化、重用、重载

💡 最佳实践

  • 优先使用包组织代码
  • 触发器逻辑尽量简单
  • 函数避免 DML(除非自治事务)
  • 所有 DML 操作考虑异常处理和事务控制

掌握本章内容,即可构建健壮、可维护的 Oracle 数据库应用逻辑。

相关推荐
l1t2 小时前
净化SQL的PL/pgSQL函数
数据库·sql·postgresql
程序员敲代码吗2 小时前
如何从Python初学者进阶为专家?
jvm·数据库·python
liux35282 小时前
MySQL日志系统全面解析:从基础到高级管理(六)
数据库·mysql·oracle
漏刻有时2 小时前
微信小程序学习实录14:微信小程序手写签名功能完整开发方案
学习·微信小程序·notepad++
a努力。2 小时前
宇树Java面试被问:数据库死锁检测和自动回滚机制
java·数据库·elasticsearch·面试·职场和发展·rpc·jenkins
数据知道2 小时前
PostgreSQL实战:详解权限设置与管理全流程
数据库·postgresql
aa765aa2 小时前
VM OS Oracle Linux 8.10 安装Oracle19C
oracle
魔芋红茶2 小时前
Spring Security 学习笔记 1:快速开始
笔记·学习·spring
皮蛋sol周2 小时前
嵌入式学习数据结构(三)栈 链式 循环队列
arm开发·数据结构·学习·算法··循环队列·链式队列