MySQL 触发器 存储过程 介绍

一、基础概念

1.1 存储过程 (Stored Procedure)

  • 定义:预编译的SQL语句集合,存储在数据库中

  • 作用:封装复杂业务逻辑,提高性能,增强安全性

  • 特点:可带参数,可返回结果

1.2 触发器 (Trigger)

  • 定义:特殊的存储过程,在特定事件发生时自动执行

  • 触发事件:INSERT、UPDATE、DELETE

  • 执行时机:BEFORE(之前)、AFTER(之后)、INSTEAD OF(替代)

  • 级别:行级触发器、语句级触发器

二、存储过程详解

MySQL示例

2.1 基本语法

sql 复制代码
-- 创建存储过程
DELIMITER //
CREATE PROCEDURE GetEmployeeByDept(
    IN dept_id INT,
    OUT emp_count INT
)
BEGIN
    -- 业务逻辑
    SELECT * FROM employees 
    WHERE department_id = dept_id;
    
    -- 设置输出参数
    SELECT COUNT(*) INTO emp_count 
    FROM employees 
    WHERE department_id = dept_id;
END //
DELIMITER ;

-- 调用存储过程
CALL GetEmployeeByDept(10, @count);
SELECT @count;

2.2 带控制流的存储过程

sql 复制代码
DELIMITER //
CREATE PROCEDURE ProcessSalary(
    IN emp_id INT,
    IN raise_percent DECIMAL(5,2)
)
BEGIN
    DECLARE current_salary DECIMAL(10,2);
    DECLARE new_salary DECIMAL(10,2);
    DECLARE max_salary DECIMAL(10,2) DEFAULT 20000;
    
    -- 异常处理
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SELECT '错误发生,事务已回滚' AS Message;
    END;
    
    START TRANSACTION;
    
    -- 获取当前工资
    SELECT salary INTO current_salary 
    FROM employees 
    WHERE id = emp_id;
    
    -- 验证数据
    IF current_salary IS NULL THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = '员工不存在';
    END IF;
    
    -- 计算新工资
    SET new_salary = current_salary * (1 + raise_percent/100);
    
    -- 检查上限
    IF new_salary > max_salary THEN
        SET new_salary = max_salary;
    END IF;
    
    -- 更新工资
    UPDATE employees 
    SET salary = new_salary,
        last_raise_date = CURDATE()
    WHERE id = emp_id;
    
    -- 记录日志
    INSERT INTO salary_history 
    (employee_id, old_salary, new_salary, change_date)
    VALUES (emp_id, current_salary, new_salary, NOW());
    
    COMMIT;
    
    SELECT 
        '工资调整完成' AS Message,
        emp_id AS EmployeeID,
        current_salary AS OldSalary,
        new_salary AS NewSalary;
END //
DELIMITER ;

三、触发器详解

3.1 基本语法

sql 复制代码
-- 创建BEFORE INSERT触发器
DELIMITER //
CREATE TRIGGER trg_before_insert_employee
BEFORE INSERT ON employees
FOR EACH ROW
BEGIN
    -- 自动设置创建时间
    SET NEW.created_at = NOW();
    
    -- 数据验证
    IF NEW.salary < 0 THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = '工资不能为负数';
    END IF;
    
    -- 数据转换
    SET NEW.email = LOWER(TRIM(NEW.email));
END //
DELIMITER ;

-- 创建AFTER UPDATE触发器
DELIMITER //
CREATE TRIGGER trg_after_update_employee
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
    -- 记录变更历史
    IF OLD.salary != NEW.salary THEN
        INSERT INTO salary_audit (
            employee_id,
            old_salary,
            new_salary,
            changed_by,
            change_date
        ) VALUES (
            NEW.id,
            OLD.salary,
            NEW.salary,
            CURRENT_USER(),
            NOW()
        );
    END IF;
    
    -- 更新部门总工资缓存
    UPDATE department_stats ds
    SET total_salary = (
        SELECT SUM(salary) 
        FROM employees 
        WHERE department_id = NEW.department_id
    )
    WHERE ds.department_id = NEW.department_id;
END //
DELIMITER ;

3.2 高级触发器示例

sql 复制代码
-- 层级审计触发器
DELIMITER //
CREATE TRIGGER trg_audit_employee_changes
AFTER INSERT OR UPDATE OR DELETE ON employees
FOR EACH ROW
BEGIN
    DECLARE operation_type VARCHAR(10);
    DECLARE old_data JSON;
    DECLARE new_data JSON;
    
    -- 确定操作类型
    IF INSERTING THEN
        SET operation_type = 'INSERT';
        SET new_data = JSON_OBJECT(
            'id', NEW.id,
            'name', NEW.name,
            'salary', NEW.salary,
            'dept_id', NEW.department_id
        );
        SET old_data = NULL;
    ELSEIF UPDATING THEN
        SET operation_type = 'UPDATE';
        SET old_data = JSON_OBJECT(
            'id', OLD.id,
            'name', OLD.name,
            'salary', OLD.salary,
            'dept_id', OLD.department_id
        );
        SET new_data = JSON_OBJECT(
            'id', NEW.id,
            'name', NEW.name,
            'salary', NEW.salary,
            'dept_id', NEW.department_id
        );
    ELSE
        SET operation_type = 'DELETE';
        SET old_data = JSON_OBJECT(
            'id', OLD.id,
            'name', OLD.name,
            'salary', OLD.salary,
            'dept_id', OLD.department_id
        );
        SET new_data = NULL;
    END IF;
    
    -- 插入审计记录
    INSERT INTO comprehensive_audit (
        table_name,
        operation_type,
        old_data,
        new_data,
        changed_by,
        change_timestamp,
        transaction_id
    ) VALUES (
        'employees',
        operation_type,
        old_data,
        new_data,
        CURRENT_USER(),
        NOW(),
        @transaction_id
    );
END //
DELIMITER ;

四、实战案例

4.1 库存管理系统

sql 复制代码
-- 库存表
CREATE TABLE products (
    product_id INT PRIMARY KEY,
    product_name VARCHAR(100),
    stock_quantity INT,
    min_stock_level INT,
    last_restock_date DATE
);

-- 订单表
CREATE TABLE orders (
    order_id INT PRIMARY KEY AUTO_INCREMENT,
    product_id INT,
    quantity INT,
    order_date DATETIME,
    status VARCHAR(20)
);

-- 库存更新存储过程
DELIMITER //
CREATE PROCEDURE sp_ProcessOrder(
    IN p_product_id INT,
    IN p_quantity INT,
    OUT p_result VARCHAR(100)
)
BEGIN
    DECLARE v_current_stock INT;
    DECLARE v_min_stock INT;
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SET p_result = '处理订单时发生错误';
    END;
    
    START TRANSACTION;
    
    -- 检查库存
    SELECT stock_quantity, min_stock_level 
    INTO v_current_stock, v_min_stock
    FROM products 
    WHERE product_id = p_product_id 
    FOR UPDATE;  -- 行锁防止并发问题
    
    IF v_current_stock IS NULL THEN
        SET p_result = '产品不存在';
        ROLLBACK;
        RETURN;
    END IF;
    
    IF v_current_stock < p_quantity THEN
        SET p_result = CONCAT('库存不足。当前库存:', v_current_stock, ', 需要:', p_quantity);
        ROLLBACK;
        RETURN;
    END IF;
    
    -- 更新库存
    UPDATE products 
    SET stock_quantity = stock_quantity - p_quantity,
        last_restock_date = CASE 
            WHEN (stock_quantity - p_quantity) <= min_stock_level 
            THEN CURDATE() 
            ELSE last_restock_date 
        END
    WHERE product_id = p_product_id;
    
    -- 创建订单记录
    INSERT INTO orders (product_id, quantity, order_date, status)
    VALUES (p_product_id, p_quantity, NOW(), '已完成');
    
    -- 检查是否需要补货
    IF (v_current_stock - p_quantity) <= v_min_stock THEN
        INSERT INTO restock_alerts (product_id, alert_date, message)
        VALUES (p_product_id, NOW(), CONCAT('产品库存低于最小库存水平。当前库存:', v_current_stock - p_quantity));
    END IF;
    
    COMMIT;
    SET p_result = '订单处理成功';
END //
DELIMITER ;

-- 库存审计触发器
DELIMITER //
CREATE TRIGGER trg_stock_audit
AFTER UPDATE ON products
FOR EACH ROW
BEGIN
    IF OLD.stock_quantity != NEW.stock_quantity THEN
        INSERT INTO stock_transactions (
            product_id,
            old_quantity,
            new_quantity,
            change_amount,
            change_type,
            changed_by,
            change_date
        ) VALUES (
            NEW.product_id,
            OLD.stock_quantity,
            NEW.stock_quantity,
            NEW.stock_quantity - OLD.stock_quantity,
            CASE 
                WHEN NEW.stock_quantity > OLD.stock_quantity THEN '入库'
                ELSE '出库'
            END,
            CURRENT_USER(),
            NOW()
        );
    END IF;
END //
DELIMITER ;

五、最佳实践

5.1 存储过程最佳实践

  1. 命名规范

    sql 复制代码
    -- 使用有意义的名称
    usp_GetCustomerOrders  -- SQL Server: usp = User Stored Procedure
    sp_calculate_monthly_report
  2. 参数验证

    sql 复制代码
    CREATE PROCEDURE sp_SafeOperation
        @InputParam INT
    AS
    BEGIN
        IF @InputParam IS NULL OR @InputParam <= 0
        BEGIN
            RAISERROR('参数必须为正整数', 16, 1);
            RETURN;
        END
        -- 主逻辑...
    END
  3. 错误处理

    sql 复制代码
    DECLARE @ErrorNumber INT;
    DECLARE @ErrorSeverity INT;
    DECLARE @ErrorState INT;
    DECLARE @ErrorProcedure NVARCHAR(128);
    DECLARE @ErrorLine INT;
    DECLARE @ErrorMessage NVARCHAR(4000);
    
    BEGIN TRY
        -- 业务逻辑
    END TRY
    BEGIN CATCH
        SELECT 
            @ErrorNumber = ERROR_NUMBER(),
            @ErrorMessage = ERROR_MESSAGE();
        -- 记录错误日志
    END CATCH

5.2 触发器最佳实践

  1. 避免递归触发

    sql 复制代码
    -- 在SQL Server中禁用递归
    ALTER DATABASE dbname SET RECURSIVE_TRIGGERS OFF;
    
    -- 在触发器中检查递归
    CREATE TRIGGER trg_no_recursion
    ON table1
    AFTER UPDATE
    AS
    BEGIN
        IF TRIGGER_NESTLEVEL() > 1
            RETURN;
        -- 逻辑...
    END
  2. 性能优化

    sql 复制代码
    -- 只处理实际变化的行
    CREATE TRIGGER trg_efficient
    AFTER UPDATE ON large_table
    FOR EACH ROW
    BEGIN
        IF NEW.column1 != OLD.column1 OR
           NEW.column2 != OLD.column2 THEN
            -- 仅当相关列变化时执行
        END IF;
    END

六、调试与维护

6.1 调试技巧

sql 复制代码
-- 临时调试表
CREATE TABLE debug_log (
    id INT AUTO_INCREMENT PRIMARY KEY,
    message TEXT,
    created_at DATETIME DEFAULT NOW()
);

-- 调试存储过程
DELIMITER //
CREATE PROCEDURE sp_DebugDemo()
BEGIN
    DECLARE debug_id INT;
    
    -- 记录开始
    INSERT INTO debug_log (message) 
    VALUES ('开始执行存储过程');
    SET debug_id = LAST_INSERT_ID();
    
    -- ... 业务逻辑
    
    -- 记录变量值
    INSERT INTO debug_log (message)
    VALUES (CONCAT('变量值: ', @variable));
    
    -- 记录结束
    UPDATE debug_log 
    SET message = CONCAT(message, ' - 执行完成')
    WHERE id = debug_id;
END //
DELIMITER ;

6.2 系统视图查询

sql 复制代码
-- MySQL: 查看存储过程和触发器
SHOW PROCEDURE STATUS;
SHOW CREATE PROCEDURE procedure_name;
SHOW TRIGGERS;
SHOW CREATE TRIGGER trigger_name;

-- 从information_schema查询
SELECT * FROM information_schema.ROUTINES 
WHERE ROUTINE_TYPE = 'PROCEDURE';

SELECT * FROM information_schema.TRIGGERS;

-- SQL Server: 查看对象定义
SELECT OBJECT_DEFINITION(OBJECT_ID('sp_name'));
EXEC sp_helptext 'sp_name';

-- 查看触发器依赖
EXEC sp_depends 'trigger_name';

七、常见问题与解决方案

问题1:触发器死锁

解决方案

sql 复制代码
-- 设置适当的隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 按相同顺序访问表
-- 减少事务持有时间

问题2:存储过程性能问题

优化方案

sql 复制代码
-- 1. 使用临时表减少重复查询
CREATE TEMPORARY TABLE temp_results AS
SELECT ...;

-- 2. 添加适当的索引
-- 3. 避免在循环中查询数据库
-- 4. 使用SET NOCOUNT ON减少网络流量

问题3:维护复杂触发器逻辑

解决方案

sql 复制代码
-- 1. 拆分为多个简单触发器
-- 2. 使用存储过程封装复杂逻辑
-- 3. 添加详细注释
-- 4. 维护变更文档

八、练习与测试

练习1:创建银行转账系统

sql 复制代码
-- 账户表
CREATE TABLE accounts (
    account_id INT PRIMARY KEY,
    balance DECIMAL(15,2),
    last_transaction_date DATETIME
);

-- 交易表
CREATE TABLE transactions (
    transaction_id INT AUTO_INCREMENT PRIMARY KEY,
    from_account INT,
    to_account INT,
    amount DECIMAL(15,2),
    transaction_date DATETIME,
    status VARCHAR(20)
);

-- 任务:
-- 1. 创建转账存储过程,包含事务处理
-- 2. 创建触发器记录余额变更
-- 3. 创建触发器检查余额不足
-- 4. 添加每日交易限额检查

总结

存储过程和触发器是数据库编程的核心组件,掌握它们可以:

  1. 提高性能:减少网络往返,预编译执行计划

  2. 增强安全性:通过封装隐藏数据细节

  3. 保证数据一致性:实现复杂的业务规则

  4. 简化应用逻辑:将业务逻辑移至数据库层

关键原则

  • 保持触发器简单高效

  • 存储过程做好错误处理

  • 充分测试并发场景

  • 编写清晰的文档和注释

  • 定期审查和优化性能

相关推荐
杨云龙UP2 小时前
MySQL慢查询日志暴涨导致磁盘告警:slow query log膨胀至397G的生产故障排查:清理、参数优化
linux·运维·服务器·数据库·mysql
Bat U2 小时前
MySQL数据库|视图+索引
数据库·mysql
SQVIoMPLe3 小时前
[拆解LangChain执行引擎]以Actor模型的视角来看Pregel
服务器·数据库·langchain
你都会上树?3 小时前
Ubuntu22 安装PostgreSQL
数据库·postgresql
想唱rap3 小时前
线程之条件变量和生产消费模型
java·服务器·开发语言·数据库·mysql·ubuntu
RInk7oBjo3 小时前
MySQL的编译安装
数据库·mysql·adb
java资料站3 小时前
MySQL 增量同步脚本
android·数据库·mysql
ningmengjing_3 小时前
从零推导出 Redis
数据库·redis
殷紫川3 小时前
InnoDB 索引性能天花板:聚簇 & 二级索引存储本质拆解,覆盖索引零回表优化全攻略
mysql