存储过程
存储过程是一组为了完成特定功能的SQL语句集,经编译后存储在数据库中。用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。
创建存储过程
sql
-- 基本语法
CREATE [OR REPLACE] PROCEDURE procedure_name
[(parameter_name [mode] datatype [, parameter_name [mode] datatype]...)]
IS
-- 声明部分
BEGIN
-- 执行部分
EXCEPTION
-- 异常处理部分
END procedure_name;
/
-- 示例:创建一个简单的存储过程
CREATE OR REPLACE PROCEDURE greet_user(p_name VARCHAR2)
IS
BEGIN
DBMS_OUTPUT.PUT_LINE('Hello, ' || p_name || '!');
END greet_user;
/
-- 调用存储过程
BEGIN
greet_user('John');
END;
/
存储过程参数模式
sql
-- IN参数(默认):用于向存储过程传递值
CREATE OR REPLACE PROCEDURE calculate_bonus(
p_employee_id IN NUMBER,
p_bonus_percent IN NUMBER
)
IS
v_salary NUMBER;
v_bonus NUMBER;
BEGIN
SELECT salary INTO v_salary
FROM employees
WHERE employee_id = p_employee_id;
v_bonus := v_salary * p_bonus_percent / 100;
DBMS_OUTPUT.PUT_LINE('Employee ID: ' || p_employee_id);
DBMS_OUTPUT.PUT_LINE('Bonus Amount: ' || v_bonus);
END calculate_bonus;
/
-- OUT参数:用于从存储过程返回值
CREATE OR REPLACE PROCEDURE get_employee_info(
p_employee_id IN NUMBER,
p_first_name OUT VARCHAR2,
p_last_name OUT VARCHAR2,
p_salary OUT NUMBER
)
IS
BEGIN
SELECT first_name, last_name, salary
INTO p_first_name, p_last_name, p_salary
FROM employees
WHERE employee_id = p_employee_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
p_first_name := NULL;
p_last_name := NULL;
p_salary := NULL;
END get_employee_info;
/
-- 调用带OUT参数的存储过程
DECLARE
v_fname VARCHAR2(20);
v_lname VARCHAR2(25);
v_salary NUMBER;
BEGIN
get_employee_info(100, v_fname, v_lname, v_salary);
DBMS_OUTPUT.PUT_LINE('Employee: ' || v_fname || ' ' || v_lname || ', Salary: ' || v_salary);
END;
/
-- IN OUT参数:既可以传递值给存储过程,也可以从存储过程中返回值
CREATE OR REPLACE PROCEDURE format_name(
p_name IN OUT VARCHAR2
)
IS
BEGIN
p_name := UPPER(p_name);
END format_name;
/
-- 调用带IN OUT参数的存储过程
DECLARE
v_name VARCHAR2(50) := 'john doe';
BEGIN
DBMS_OUTPUT.PUT_LINE('Before: ' || v_name);
format_name(v_name);
DBMS_OUTPUT.PUT_LINE('After: ' || v_name);
END;
/
复杂存储过程示例
sql
-- 创建一个复杂的存储过程,用于员工转岗
CREATE OR REPLACE PROCEDURE transfer_employee(
p_employee_id IN NUMBER,
p_new_department_id IN NUMBER,
p_new_job_id IN VARCHAR2,
p_result OUT VARCHAR2
)
IS
v_old_dept_id NUMBER;
v_old_job_id VARCHAR2(10);
v_employee_count NUMBER;
BEGIN
-- 检查员工是否存在
SELECT department_id, job_id
INTO v_old_dept_id, v_old_job_id
FROM employees
WHERE employee_id = p_employee_id;
-- 检查新部门是否存在
SELECT COUNT(*) INTO v_employee_count
FROM departments
WHERE department_id = p_new_department_id;
IF v_employee_count = 0 THEN
p_result := 'Error: Department does not exist';
RETURN;
END IF;
-- 更新员工信息
UPDATE employees
SET department_id = p_new_department_id,
job_id = p_new_job_id,
manager_id = (SELECT manager_id FROM departments WHERE department_id = p_new_department_id)
WHERE employee_id = p_employee_id;
-- 记录转岗历史
INSERT INTO job_history (employee_id, start_date, end_date, job_id, department_id)
VALUES (p_employee_id, SYSDATE, NULL, p_new_job_id, p_new_department_id);
COMMIT;
p_result := 'Success: Employee transferred successfully';
EXCEPTION
WHEN NO_DATA_FOUND THEN
p_result := 'Error: Employee not found';
ROLLBACK;
WHEN OTHERS THEN
p_result := 'Error: ' || SQLERRM;
ROLLBACK;
END transfer_employee;
/
-- 调用存储过程
DECLARE
v_result VARCHAR2(100);
BEGIN
transfer_employee(101, 60, 'IT_PROG', v_result);
DBMS_OUTPUT.PUT_LINE(v_result);
END;
/
2.2 函数
函数与存储过程类似,但函数必须返回一个值。函数可以在SQL语句中调用。
创建函数
sql
-- 基本语法
CREATE [OR REPLACE] FUNCTION function_name
[(parameter_name [mode] datatype [, parameter_name [mode] datatype]...)]
RETURN datatype
IS
-- 声明部分
BEGIN
-- 执行部分
RETURN value;
EXCEPTION
-- 异常处理部分
END function_name;
/
-- 示例:创建一个计算年薪的函数
CREATE OR REPLACE FUNCTION calculate_annual_salary(
p_monthly_salary NUMBER,
p_bonus_percent NUMBER DEFAULT 0
) RETURN NUMBER
IS
v_annual_salary NUMBER;
BEGIN
v_annual_salary := p_monthly_salary * 12;
v_annual_salary := v_annual_salary + (v_annual_salary * p_bonus_percent / 100);
RETURN v_annual_salary;
END calculate_annual_salary;
/
-- 在SQL语句中调用函数
SELECT first_name, last_name, salary,
calculate_annual_salary(salary, 10) AS annual_salary_with_bonus
FROM employees;
-- 在PL/SQL块中调用函数
DECLARE
v_annual_sal NUMBER;
BEGIN
v_annual_sal := calculate_annual_salary(5000, 15);
DBMS_OUTPUT.PUT_LINE('Annual Salary: ' || v_annual_sal);
END;
/
复杂函数示例
sql
-- 创建一个获取员工完整信息的函数
CREATE OR REPLACE FUNCTION get_employee_full_info(p_employee_id NUMBER)
RETURN VARCHAR2
IS
v_info VARCHAR2(4000);
v_first_name employees.first_name%TYPE;
v_last_name employees.last_name%TYPE;
v_salary employees.salary%TYPE;
v_department_name departments.department_name%TYPE;
BEGIN
SELECT e.first_name, e.last_name, e.salary, d.department_name
INTO v_first_name, v_last_name, v_salary, v_department_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE e.employee_id = p_employee_id;
v_info := 'Employee: ' || v_first_name || ' ' || v_last_name ||
', Salary: ' || v_salary ||
', Department: ' || v_department_name;
RETURN v_info;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN 'Employee not found';
WHEN OTHERS THEN
RETURN 'Error: ' || SQLERRM;
END get_employee_full_info;
/
-- 调用函数
BEGIN
DBMS_OUTPUT.PUT_LINE(get_employee_full_info(100));
END;
/
确定性函数
sql
-- 创建确定性函数(对于相同的输入总是返回相同的输出)
CREATE OR REPLACE FUNCTION calculate_tax(p_income NUMBER)
RETURN NUMBER
DETERMINISTIC
IS
BEGIN
IF p_income <= 50000 THEN
RETURN p_income * 0.1;
ELSIF p_income <= 100000 THEN
RETURN 5000 + (p_income - 50000) * 0.2;
ELSE
RETURN 15000 + (p_income - 100000) * 0.3;
END IF;
END calculate_tax;
/
-- 在查询中使用确定性函数
SELECT first_name, last_name, salary,
calculate_tax(salary) AS tax_amount
FROM employees
ORDER BY salary DESC;
2.3 包(Package)
包是将相关的PL/SQL类型、子程序、游标、异常、变量和常量组合在一起的模式对象。
创建包规范
sql
-- 创建包规范
CREATE OR REPLACE PACKAGE employee_pkg IS
-- 常量声明
c_min_salary CONSTANT NUMBER := 2000;
-- 变量声明
g_employee_count NUMBER;
-- 函数声明
FUNCTION get_employee_count RETURN NUMBER;
FUNCTION calculate_years_of_service(p_employee_id NUMBER) RETURN NUMBER;
-- 过程声明
PROCEDURE hire_employee(
p_first_name VARCHAR2,
p_last_name VARCHAR2,
p_email VARCHAR2,
p_phone VARCHAR2,
p_hire_date DATE,
p_job_id VARCHAR2,
p_salary NUMBER,
p_department_id NUMBER
);
PROCEDURE fire_employee(p_employee_id NUMBER);
END employee_pkg;
/
创建包体
sql
-- 创建包体
CREATE OR REPLACE PACKAGE BODY employee_pkg IS
-- 实现函数
FUNCTION get_employee_count RETURN NUMBER IS
v_count NUMBER;
BEGIN
SELECT COUNT(*) INTO v_count FROM employees;
g_employee_count := v_count;
RETURN v_count;
END get_employee_count;
FUNCTION calculate_years_of_service(p_employee_id NUMBER) RETURN NUMBER IS
v_hire_date DATE;
v_years NUMBER;
BEGIN
SELECT hire_date INTO v_hire_date
FROM employees
WHERE employee_id = p_employee_id;
v_years := ROUND(MONTHS_BETWEEN(SYSDATE, v_hire_date) / 12, 1);
RETURN v_years;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN -1;
END calculate_years_of_service;
-- 实现过程
PROCEDURE hire_employee(
p_first_name VARCHAR2,
p_last_name VARCHAR2,
p_email VARCHAR2,
p_phone VARCHAR2,
p_hire_date DATE,
p_job_id VARCHAR2,
p_salary NUMBER,
p_department_id NUMBER
) IS
v_employee_id NUMBER;
BEGIN
-- 验证薪资
IF p_salary < c_min_salary THEN
RAISE_APPLICATION_ERROR(-20001, 'Salary must be at least ' || c_min_salary);
END IF;
-- 生成员工ID
SELECT MAX(employee_id) + 1 INTO v_employee_id FROM employees;
-- 插入新员工
INSERT INTO employees (
employee_id, first_name, last_name, email, phone_number,
hire_date, job_id, salary, department_id
) VALUES (
v_employee_id, p_first_name, p_last_name, p_email, p_phone,
p_hire_date, p_job_id, p_salary, p_department_id
);
COMMIT;
DBMS_OUTPUT.PUT_LINE('Employee hired successfully with ID: ' || v_employee_id);
END hire_employee;
PROCEDURE fire_employee(p_employee_id NUMBER) IS
v_employee_name VARCHAR2(50);
BEGIN
-- 获取员工姓名
SELECT first_name || ' ' || last_name INTO v_employee_name
FROM employees
WHERE employee_id = p_employee_id;
-- 删除员工
DELETE FROM employees WHERE employee_id = p_employee_id;
COMMIT;
DBMS_OUTPUT.PUT_LINE('Employee ' || v_employee_name || ' fired successfully');
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR(-20002, 'Employee not found');
END fire_employee;
END employee_pkg;
/
使用包
sql
-- 调用包中的函数
BEGIN
DBMS_OUTPUT.PUT_LINE('Total Employees: ' || employee_pkg.get_employee_count);
DBMS_OUTPUT.PUT_LINE('Years of Service: ' || employee_pkg.calculate_years_of_service(100));
END;
/
-- 调用包中的过程
BEGIN
employee_pkg.hire_employee(
'John', 'Doe', 'JDOE@EXAMPLE.COM', '555-1234',
SYSDATE, 'IT_PROG', 7500, 60
);
employee_pkg.fire_employee(207); -- 假设这是刚刚雇佣的员工
END;
/
2.4 触发器
触发器是存储在数据库中的一段程序,当特定的事件(如INSERT、UPDATE、DELETE)发生时自动执行。
DML触发器
sql
-- 创建行级触发器(在每次行操作时触发)
CREATE OR REPLACE TRIGGER audit_employee_changes
AFTER INSERT OR UPDATE OR DELETE ON employees
FOR EACH ROW
DECLARE
v_action VARCHAR2(10);
BEGIN
IF INSERTING THEN
v_action := 'INSERT';
INSERT INTO employee_audit (
employee_id, action, action_date, user_name
) VALUES (
:NEW.employee_id, v_action, SYSDATE, USER
);
ELSIF UPDATING THEN
v_action := 'UPDATE';
INSERT INTO employee_audit (
employee_id, action, action_date, user_name,
old_salary, new_salary
) VALUES (
:OLD.employee_id, v_action, SYSDATE, USER,
:OLD.salary, :NEW.salary
);
ELSIF DELETING THEN
v_action := 'DELETE';
INSERT INTO employee_audit (
employee_id, action, action_date, user_name,
old_salary
) VALUES (
:OLD.employee_id, v_action, SYSDATE, USER,
:OLD.salary
);
END IF;
END audit_employee_changes;
/
-- 创建语句级触发器(在整个SQL语句执行前后触发)
CREATE OR REPLACE TRIGGER log_employee_updates
BEFORE UPDATE ON employees
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*) INTO v_count FROM employees WHERE salary > 20000;
DBMS_OUTPUT.PUT_LINE('Employees with salary > 20000: ' || v_count);
END log_employee_updates;
/
复合触发器
sql
-- 创建复合触发器(在一个触发器中处理所有触发事件)
CREATE OR REPLACE TRIGGER employee_compound_trigger
FOR INSERT OR UPDATE OR DELETE ON employees
COMPOUND TRIGGER
-- 声明部分
TYPE t_employee_ids IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
v_employee_ids t_employee_ids;
v_counter PLS_INTEGER := 0;
-- BEFORE STATEMENT部分
BEFORE STATEMENT IS
BEGIN
DBMS_OUTPUT.PUT_LINE('Starting employee operation');
END BEFORE STATEMENT;
-- BEFORE EACH ROW部分
BEFORE EACH ROW IS
BEGIN
IF INSERTING THEN
DBMS_OUTPUT.PUT_LINE('Inserting employee: ' || :NEW.employee_id);
ELSIF UPDATING THEN
DBMS_OUTPUT.PUT_LINE('Updating employee: ' || :OLD.employee_id);
ELSIF DELETING THEN
DBMS_OUTPUT.PUT_LINE('Deleting employee: ' || :OLD.employee_id);
END IF;
END BEFORE EACH ROW;
-- AFTER EACH ROW部分
AFTER EACH ROW IS
BEGIN
v_counter := v_counter + 1;
IF INSERTING THEN
v_employee_ids(v_counter) := :NEW.employee_id;
ELSIF UPDATING THEN
v_employee_ids(v_counter) := :OLD.employee_id;
ELSIF DELETING THEN
v_employee_ids(v_counter) := :OLD.employee_id;
END IF;
END AFTER EACH ROW;
-- AFTER STATEMENT部分
AFTER STATEMENT IS
v_total NUMBER;
BEGIN
SELECT COUNT(*) INTO v_total FROM employees;
DBMS_OUTPUT.PUT_LINE('Operation completed. Total employees: ' || v_total);
DBMS_OUTPUT.PUT_LINE('Processed ' || v_counter || ' rows');
END AFTER STATEMENT;
END employee_compound_trigger;
/
2.5 本章小结
本章详细介绍了Oracle数据库中的存储过程、函数、包和触发器等PL/SQL编程的重要组成部分。掌握这些技能可以帮助开发人员创建更加复杂和高效的数据库应用程序。
练习题
- 创建一个存储过程,接受员工ID和加薪金额作为参数,更新员工薪资
- 编写一个函数,计算两个日期之间的天数差
- 创建一个包,包含员工管理的相关函数和过程
- 编写一个触发器,在员工表更新时自动记录修改历史
- 创建一个复合触发器,处理员工表的所有DML操作
- 编写一个确定性函数,根据员工等级计算奖金系数