【Oracle入门到删库跑路-06】核心技能:存储过程和函数

存储过程

存储过程是一组为了完成特定功能的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编程的重要组成部分。掌握这些技能可以帮助开发人员创建更加复杂和高效的数据库应用程序。

练习题

  1. 创建一个存储过程,接受员工ID和加薪金额作为参数,更新员工薪资
  2. 编写一个函数,计算两个日期之间的天数差
  3. 创建一个包,包含员工管理的相关函数和过程
  4. 编写一个触发器,在员工表更新时自动记录修改历史
  5. 创建一个复合触发器,处理员工表的所有DML操作
  6. 编写一个确定性函数,根据员工等级计算奖金系数
相关推荐
LucidX1 小时前
Mysql 数据库部署
数据库·oracle
数据库学啊1 小时前
国产时序数据库哪个靠谱
数据库·时序数据库
尋有緣1 小时前
力扣2292-连续两年有3个及以上的订单产品
leetcode·oracle·数据库开发
网安老伯1 小时前
什么是网络安全?网络安全包括哪几个方面?学完能做一名黑客吗?
linux·数据库·python·web安全·网络安全·php·xss
瀚高PG实验室2 小时前
postgresql日期/时间数据类型中有无时区的差异使用
数据库·postgresql·瀚高数据库
Elastic 中国社区官方博客2 小时前
Elasticsearch 中的文档级基于属性的访问控制 - ABAC
大数据·数据库·elasticsearch·搜索引擎·全文检索
数据库学啊2 小时前
国产时序数据库供应商排名
数据库·时序数据库
songtaiwu3 小时前
golang开发日常注意
开发语言·数据库·golang
NineData3 小时前
NineData社区版V4.7.0发布!新增MySQL至TiDB等6条数据复制对比链路,SQL窗口新增谷歌云6种数据源类型
数据库·dba