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*Plus 、Oracle 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;
删除规范会自动删除主体。
六、综合性实战案例
案例:员工管理系统(含过程、函数、触发器、包)
需求:
- 创建包
hr_mgmt管理员工 - 提供入职、加薪、查询功能
- 自动记录工资变更日志
- 禁止非法操作(如工资为负)
步骤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 数据库应用逻辑。