Oracle中怎么实现存储过程

发布时间:2021-08-13 17:25:13 作者:Leah
来源:亿速云 阅读:246
# Oracle中怎么实现存储过程

## 一、存储过程概述

### 1.1 什么是存储过程
存储过程(Stored Procedure)是Oracle数据库中一组为了完成特定功能的SQL语句集合,经编译后存储在数据库中。用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。

存储过程具有以下特点:
- 预编译的SQL语句集合
- 存储在数据库服务器端
- 可通过名称和参数调用
- 支持流程控制语句
- 可返回多个结果集

### 1.2 存储过程的优势

1. **提高性能**:存储过程在创建时进行编译,后续调用无需重新编译
2. **减少网络流量**:客户端只需传递存储过程名和参数,而非完整SQL
3. **增强安全性**:可通过授权机制控制对存储过程的访问
4. **代码复用**:一次编写,多次调用
5. **模块化编程**:将复杂业务逻辑封装在数据库层

### 1.3 存储过程与函数的区别

| 特性                | 存储过程                  | 函数                      |
|---------------------|-------------------------|-------------------------|
| 返回值              | 可以没有或返回多个       | 必须返回一个值          |
| 调用方式            | EXECUTE或CALL语句        | 在SQL语句中直接调用      |
| 参数模式            | IN,OUT,IN OUT           | 只有IN模式              |
| DML操作             | 可以执行                 | 通常不执行(除非是自治事务)|
| 异常处理            | 有完善的异常处理机制     | 异常处理较简单          |

## 二、存储过程基础语法

### 2.1 创建存储过程

基本语法结构:
```sql
CREATE [OR REPLACE] PROCEDURE procedure_name
    [ (parameter [,parameter]) ]
IS
    [declaration_section]
BEGIN
    executable_section
[EXCEPTION
    exception_section]
END [procedure_name];

示例:创建一个简单存储过程

CREATE OR REPLACE PROCEDURE greet_employee
IS
BEGIN
    DBMS_OUTPUT.PUT_LINE('Hello, employees!');
END greet_employee;

2.2 参数模式

Oracle存储过程支持三种参数模式:

  1. IN参数(默认模式):传入参数,过程内部不能修改

    CREATE OR REPLACE PROCEDURE raise_salary (
       p_emp_id IN NUMBER,
       p_amount IN NUMBER
    ) IS
    BEGIN
       UPDATE employees 
       SET salary = salary + p_amount
       WHERE employee_id = p_emp_id;
    END raise_salary;
    
  2. OUT参数:输出参数,过程内部可修改并返回

    CREATE OR REPLACE PROCEDURE get_employee_info (
       p_emp_id IN NUMBER,
       p_name OUT VARCHAR2,
       p_salary OUT NUMBER
    ) IS
    BEGIN
       SELECT last_name, salary
       INTO p_name, p_salary
       FROM employees
       WHERE employee_id = p_emp_id;
    END get_employee_info;
    
  3. IN OUT参数:双向参数,传入值并可被修改后返回

    CREATE OR REPLACE PROCEDURE format_phone_number (
       p_phone IN OUT VARCHAR2
    ) IS
    BEGIN
       p_phone := '(' || SUBSTR(p_phone,1,3) || ') ' || 
                  SUBSTR(p_phone,4,3) || '-' ||
                  SUBSTR(p_phone,7);
    END format_phone_number;
    

2.3 调用存储过程

  1. 使用EXECUTE命令:

    EXECUTE procedure_name(parameters);
    
  2. 使用CALL语句:

    CALL procedure_name(parameters);
    
  3. 在PL/SQL块中调用:

    BEGIN
       procedure_name(parameters);
    END;
    

带OUT参数调用的示例:

DECLARE
    v_name VARCHAR2(100);
    v_salary NUMBER;
BEGIN
    get_employee_info(100, v_name, v_salary);
    DBMS_OUTPUT.PUT_LINE('Name: ' || v_name || ', Salary: ' || v_salary);
END;

三、高级存储过程特性

3.1 异常处理

存储过程中可以使用完整的异常处理机制:

CREATE OR REPLACE PROCEDURE process_order (
    p_order_id IN NUMBER
) IS
    v_status VARCHAR2(20);
BEGIN
    -- 业务逻辑
    SELECT status INTO v_status FROM orders WHERE order_id = p_order_id;
    
    IF v_status != 'OPEN' THEN
        RSE_APPLICATION_ERROR(-20001, 'Order is not open');
    END IF;
    
    -- 更多处理逻辑
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('Order not found');
    WHEN TOO_MANY_ROWS THEN
        DBMS_OUTPUT.PUT_LINE('Multiple orders found');
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
        ROLLBACK;
END process_order;

3.2 事务控制

存储过程中可以包含完整的事务控制语句:

CREATE OR REPLACE PROCEDURE transfer_funds (
    p_from_acct IN NUMBER,
    p_to_acct IN NUMBER,
    p_amount IN NUMBER
) IS
    v_balance NUMBER;
BEGIN
    -- 检查账户余额
    SELECT balance INTO v_balance 
    FROM accounts 
    WHERE account_id = p_from_acct
    FOR UPDATE;  -- 加锁
    
    IF v_balance < p_amount THEN
        RSE_APPLICATION_ERROR(-20002, 'Insufficient funds');
    END IF;
    
    -- 转账操作
    UPDATE accounts SET balance = balance - p_amount
    WHERE account_id = p_from_acct;
    
    UPDATE accounts SET balance = balance + p_amount
    WHERE account_id = p_to_acct;
    
    -- 记录交易
    INSERT INTO transactions VALUES (
        trans_seq.NEXTVAL, 
        p_from_acct, 
        p_to_acct, 
        p_amount, 
        SYSDATE
    );
    
    COMMIT;
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        RSE;
END transfer_funds;

3.3 游标使用

存储过程中可以使用游标处理多行数据:

CREATE OR REPLACE PROCEDURE update_department_salaries (
    p_dept_id IN NUMBER,
    p_percent IN NUMBER
) IS
    CURSOR emp_cur IS
        SELECT employee_id, salary
        FROM employees
        WHERE department_id = p_dept_id
        FOR UPDATE OF salary;
        
    v_count NUMBER := 0;
BEGIN
    FOR emp_rec IN emp_cur LOOP
        UPDATE employees
        SET salary = salary * (1 + p_percent/100)
        WHERE CURRENT OF emp_cur;
        
        v_count := v_count + 1;
    END LOOP;
    
    DBMS_OUTPUT.PUT_LINE('Updated ' || v_count || ' employees');
    COMMIT;
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
END update_department_salaries;

四、存储过程最佳实践

4.1 命名规范

推荐采用以下命名约定: - 过程名:动词开头,如calculate_totals - 参数名:前缀表示模式,如p_表示参数,v_表示变量 - 游标名:cur_前缀 - 异常名:e_前缀

4.2 性能优化建议

  1. 使用NOCOPY提示减少大型OUT/IN OUT参数的复制开销:

    CREATE PROCEDURE process_large_data (
       p_data IN OUT NOCOPY CLOB
    ) IS
    
  2. 批量操作使用FORALL代替循环:

    CREATE PROCEDURE bulk_update IS
       TYPE id_array IS TABLE OF NUMBER;
       v_ids id_array := id_array(101, 102, 103);
    BEGIN
       FORALL i IN 1..v_ids.COUNT
           UPDATE employees SET salary = salary * 1.1
           WHERE employee_id = v_ids(i);
    END;
    
  3. 使用绑定变量而非字符串拼接: “`sql – 不推荐 EXECUTE IMMEDIATE ‘SELECT * FROM ’ || p_table_name;

– 推荐 EXECUTE IMMEDIATE ‘SELECT * FROM ’ || DBMS_ASSERT.SQL_OBJECT_NAME(p_table_name);


### 4.3 安全考虑

1. 使用`AUTHID`子句控制执行权限:
   ```sql
   CREATE OR REPLACE PROCEDURE sensitive_operation
   AUTHID DEFINER IS  -- 以定义者权限执行
   BEGIN
       -- 访问定义者有权限但调用者无权限的对象
   END;
  1. 防范SQL注入:

    CREATE PROCEDURE search_products (
       p_name IN VARCHAR2
    ) IS
       v_sql VARCHAR2(1000);
    BEGIN
       -- 不安全
       -- v_sql := 'SELECT * FROM products WHERE name LIKE ''%' || p_name || '%''';
    
    
       -- 安全方式
       v_sql := 'SELECT * FROM products WHERE name LIKE ''%'' || :name || ''%''';
       EXECUTE IMMEDIATE v_sql USING p_name;
    END;
    

五、实际应用案例

5.1 数据迁移存储过程

CREATE OR REPLACE PROCEDURE migrate_customer_data AS
    CURSOR cust_cur IS
        SELECT customer_id, old_credit_limit, old_status
        FROM legacy_customers
        WHERE migration_flag = 'N';
        
    v_count NUMBER := 0;
    v_start_time TIMESTAMP := SYSTIMESTAMP;
BEGIN
    FOR cust_rec IN cust_cur LOOP
        BEGIN
            -- 插入新系统
            INSERT INTO customers (
                customer_id, 
                credit_limit, 
                status,
                create_date
            ) VALUES (
                cust_rec.customer_id,
                cust_rec.old_credit_limit * 1.1, -- 新系统信用额度增加10%
                CASE cust_rec.old_status
                    WHEN 'A' THEN 'ACTIVE'
                    WHEN 'I' THEN 'INACTIVE'
                    ELSE 'PENDING'
                END,
                SYSDATE
            );
            
            -- 更新迁移标志
            UPDATE legacy_customers
            SET migration_flag = 'Y',
                migration_date = SYSDATE
            WHERE customer_id = cust_rec.customer_id;
            
            v_count := v_count + 1;
            
            -- 每100条提交一次
            IF MOD(v_count, 100) = 0 THEN
                COMMIT;
            END IF;
        EXCEPTION
            WHEN DUP_VAL_ON_INDEX THEN
                DBMS_OUTPUT.PUT_LINE('Duplicate customer: ' || cust_rec.customer_id);
                UPDATE legacy_customers
                SET migration_flag = 'E',
                    migration_error = 'Duplicate ID'
                WHERE customer_id = cust_rec.customer_id;
            WHEN OTHERS THEN
                DBMS_OUTPUT.PUT_LINE('Error migrating customer ' || 
                    cust_rec.customer_id || ': ' || SQLERRM);
                UPDATE legacy_customers
                SET migration_flag = 'E',
                    migration_error = SUBSTR(SQLERRM, 1, 200)
                WHERE customer_id = cust_rec.customer_id;
        END;
    END LOOP;
    
    COMMIT;
    
    DBMS_OUTPUT.PUT_LINE('Migration completed');
    DBMS_OUTPUT.PUT_LINE('Total records processed: ' || v_count);
    DBMS_OUTPUT.PUT_LINE('Elapsed time: ' || 
        EXTRACT(SECOND FROM (SYSTIMESTAMP - v_start_time)) || ' seconds');
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        DBMS_OUTPUT.PUT_LINE('Critical error: ' || SQLERRM);
        RSE;
END migrate_customer_data;

5.2 复杂报表生成

CREATE OR REPLACE PROCEDURE generate_monthly_sales_report (
    p_month IN NUMBER,
    p_year IN NUMBER,
    p_dept_id IN NUMBER DEFAULT NULL
) AS
    v_report_id NUMBER;
    v_start_date DATE;
    v_end_date DATE;
    v_dept_name VARCHAR2(100);
    
    -- 定义记录类型
    TYPE sales_rec IS RECORD (
        product_id NUMBER,
        product_name VARCHAR2(100),
        total_quantity NUMBER,
        total_amount NUMBER(15,2)
    );
    
    -- 定义表类型
    TYPE sales_table IS TABLE OF sales_rec;
    
    -- 声明变量
    v_sales_data sales_table;
    v_total_sales NUMBER(15,2) := 0;
    v_total_quantity NUMBER := 0;
BEGIN
    -- 计算日期范围
    v_start_date := TO_DATE('01-' || p_month || '-' || p_year, 'DD-MM-YYYY');
    v_end_date := LAST_DAY(v_start_date);
    
    -- 获取部门名称(如果指定了部门)
    IF p_dept_id IS NOT NULL THEN
        BEGIN
            SELECT department_name INTO v_dept_name
            FROM departments
            WHERE department_id = p_dept_id;
        EXCEPTION
            WHEN NO_DATA_FOUND THEN
                v_dept_name := 'Department ' || p_dept_id || ' (Not Found)';
        END;
    ELSE
        v_dept_name := 'All Departments';
    END IF;
    
    -- 生成唯一报告ID
    SELECT report_seq.NEXTVAL INTO v_report_id FROM dual;
    
    -- 插入报告头信息
    INSERT INTO sales_reports (
        report_id,
        report_month,
        report_year,
        department_id,
        department_name,
        generation_date
    ) VALUES (
        v_report_id,
        p_month,
        p_year,
        p_dept_id,
        v_dept_name,
        SYSDATE
    );
    
    -- 获取销售数据
    IF p_dept_id IS NULL THEN
        -- 全公司销售数据
        SELECT 
            p.product_id,
            p.product_name,
            SUM(oi.quantity),
            SUM(oi.quantity * oi.unit_price)
        BULK COLLECT INTO v_sales_data
        FROM order_items oi
        JOIN products p ON oi.product_id = p.product_id
        JOIN orders o ON oi.order_id = o.order_id
        WHERE o.order_date BETWEEN v_start_date AND v_end_date
        GROUP BY p.product_id, p.product_name
        ORDER BY SUM(oi.quantity * oi.unit_price) DESC;
    ELSE
        -- 特定部门销售数据
        SELECT 
            p.product_id,
            p.product_name,
            SUM(oi.quantity),
            SUM(oi.quantity * oi.unit_price)
        BULK COLLECT INTO v_sales_data
        FROM order_items oi
        JOIN products p ON oi.product_id = p.product_id
        JOIN orders o ON oi.order_id = o.order_id
        JOIN employees e ON o.sales_rep_id = e.employee_id
        WHERE o.order_date BETWEEN v_start_date AND v_end_date
        AND e.department_id = p_dept_id
        GROUP BY p.product_id, p.product_name
        ORDER BY SUM(oi.quantity * oi.unit_price) DESC;
    END IF;
    
    -- 处理销售数据
    FOR i IN 1..v_sales_data.COUNT LOOP
        -- 插入明细记录
        INSERT INTO sales_report_details (
            report_id,
            line_number,
            product_id,
            product_name,
            quantity,
            amount
        ) VALUES (
            v_report_id,
            i,
            v_sales_data(i).product_id,
            v_sales_data(i).product_name,
            v_sales_data(i).total_quantity,
            v_sales_data(i).total_amount
        );
        
        -- 累计总数
        v_total_quantity := v_total_quantity + v_sales_data(i).total_quantity;
        v_total_sales := v_total_sales + v_sales_data(i).total_amount;
    END LOOP;
    
    -- 更新报告汇总信息
    UPDATE sales_reports
    SET total_quantity = v_total_quantity,
        total_amount = v_total_sales,
        record_count = v_sales_data.COUNT
    WHERE report_id = v_report_id;
    
    COMMIT;
    
    DBMS_OUTPUT.PUT_LINE('Report generated successfully. ID: ' || v_report_id);
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        DBMS_OUTPUT.PUT_LINE('Error generating report: ' || SQLERRM);
        RSE;
END generate_monthly_sales_report;

六、调试与维护

6.1 调试技术

  1. 使用DBMS_OUTPUT进行简单调试:

    CREATE PROCEDURE debug_example IS
       v_temp NUMBER := 0;
    BEGIN
       DBMS_OUTPUT.PUT_LINE('Starting procedure');
    
    
       FOR i IN 1..10 LOOP
           v_temp := v_temp + i;
           DBMS_OUTPUT.PUT_LINE('Iteration ' || i || ': ' || v_temp);
       END LOOP;
    
    
       DBMS_OUTPUT.PUT_LINE('Final total: ' || v_temp);
    END;
    
  2. 使用条件编译进行调试: “`sql CREATE PROCEDURE debug_with_cc IS BEGIN \(IF \)\(DEBUG \)THEN DBMS_OUTPUT.PUT_LINE(‘Debug mode: Starting procedure’); $END

    – 业务逻辑

    \(IF \)\(DEBUG \)THEN DBMS_OUTPUT.PUT_LINE(‘Debug mode: Procedure completed’); $END END;

– 启用调试 ALTER PROCEDURE debug_with_cc COMPILE PLSQL_CCFLAGS = ‘DEBUG:TRUE’ REUSE SETTINGS; “`

6.2 维护建议

  1. 添加注释头: “`sql /*
    • Procedure: calculate_bonus
    • Author: John
推荐阅读:
  1. oracle中如何调用存储过程
  2. oracle存储过程

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

oracle

上一篇:C++中怎样实现快速排序

下一篇:javascript中怎么隐藏电子邮件地址

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》