Skip to content
On this page

MySQL触发器

触发器是在特定数据库事件(如INSERT、UPDATE、DELETE)发生时自动执行的特殊存储程序。它们提供了一种强大而灵活的方式来维护数据完整性、实施业务规则和自动化数据库操作。

触发器基础

触发器概念

触发器是与表相关联的数据库对象,当表上发生特定事件时自动执行。触发器具有以下特征:

  • 自动执行:无需手动调用
  • 事件驱动:响应INSERT、UPDATE、DELETE操作
  • 表级关联:每个触发器都与特定表关联
  • 时机控制:可在事件发生前或后执行

创建触发器语法

sql
DELIMITER //
CREATE TRIGGER trigger_name
trigger_time trigger_event
ON table_name FOR EACH ROW
BEGIN
    -- 触发器逻辑
END //
DELIMITER ;

-- trigger_time: BEFORE 或 AFTER
-- trigger_event: INSERT、UPDATE 或 DELETE

触发器类型

INSERT触发器

在插入新记录时执行的触发器。

sql
-- 创建用户表
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 创建INSERT触发器:自动设置创建时间
DELIMITER //
CREATE TRIGGER tr_users_insert
BEFORE INSERT ON users
FOR EACH ROW
BEGIN
    SET NEW.created_at = NOW();
    SET NEW.updated_at = NOW();
    
    -- 记录操作日志
    INSERT INTO audit_log (table_name, operation, record_id, timestamp) 
    VALUES ('users', 'INSERT', NEW.id, NOW());
END //
DELIMITER ;

UPDATE触发器

在更新记录时执行的触发器。

sql
-- 创建UPDATE触发器:记录更新前后值的变化
DELIMITER //
CREATE TRIGGER tr_users_update
BEFORE UPDATE ON users
FOR EACH ROW
BEGIN
    SET NEW.updated_at = NOW();
    
    -- 如果邮箱发生变化,记录变更日志
    IF OLD.email != NEW.email THEN
        INSERT INTO change_log (table_name, record_id, field_name, old_value, new_value, changed_at)
        VALUES ('users', NEW.id, 'email', OLD.email, NEW.email, NOW());
    END IF;
END //
DELIMITER ;

DELETE触发器

在删除记录时执行的触发器。

sql
-- 创建DELETE触发器:软删除而不是物理删除
DELIMITER //
CREATE TRIGGER tr_users_delete
BEFORE DELETE ON users
FOR EACH ROW
BEGIN
    -- 插入到历史表而不是真正删除
    INSERT INTO users_history (id, username, email, created_at, updated_at, deleted_at)
    VALUES (OLD.id, OLD.username, OLD.email, OLD.created_at, OLD.updated_at, NOW());
    
    -- 记录删除操作
    INSERT INTO audit_log (table_name, operation, record_id, timestamp) 
    VALUES ('users', 'DELETE', OLD.id, NOW());
    
    -- 阻止物理删除
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '请勿直接删除用户,请使用更新状态的方式';
END //
DELIMITER ;

-- 实际实现软删除的触发器
DELIMITER //
CREATE TRIGGER tr_users_soft_delete
BEFORE DELETE ON users
FOR EACH ROW
BEGIN
    -- 软删除:更新状态而不是物理删除
    UPDATE users SET status = 'deleted', deleted_at = NOW() WHERE id = OLD.id;
    
    -- 记录操作
    INSERT INTO audit_log (table_name, operation, record_id, timestamp) 
    VALUES ('users', 'SOFT_DELETE', OLD.id, NOW());
    
    -- 阻止实际删除操作
    SET @delete_blocked = 1;
END //
DELIMITER ;

-- 更实用的删除触发器:将记录移动到归档表
CREATE TABLE users_archive LIKE users;

DELIMITER //
CREATE TRIGGER tr_users_archive_delete
AFTER DELETE ON users
FOR EACH ROW
BEGIN
    INSERT INTO users_archive (id, username, email, created_at, updated_at)
    VALUES (OLD.id, OLD.username, OLD.email, OLD.created_at, OLD.updated_at);
END //
DELIMITER ;

NEW和OLD关键字

在触发器中,NEW和OLD关键字用于引用受影响的行:

  • NEW:在INSERT和UPDATE触发器中,引用即将插入或更新的行
  • OLD:在UPDATE和DELETE触发器中,引用更新前或删除前的行
sql
-- 示例:验证更新操作
DELIMITER //
CREATE TRIGGER tr_validate_user_update
BEFORE UPDATE ON users
FOR EACH ROW
BEGIN
    -- 验证邮箱格式(简单验证)
    IF NEW.email NOT REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$' THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '邮箱格式无效';
    END IF;
    
    -- 防止重要用户信息被意外修改
    IF OLD.username != NEW.username AND OLD.username LIKE 'admin%' THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '管理员用户名不能被修改';
    END IF;
END //
DELIMITER ;

实际应用场景

1. 审计日志

sql
-- 创建审计日志表
CREATE TABLE audit_log (
    id INT AUTO_INCREMENT PRIMARY KEY,
    table_name VARCHAR(50),
    operation VARCHAR(10), -- INSERT, UPDATE, DELETE
    record_id INT,
    old_values JSON,
    new_values JSON,
    user_name VARCHAR(50),
    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 创建通用审计触发器
DELIMITER //
CREATE TRIGGER tr_users_audit
AFTER UPDATE ON users
FOR EACH ROW
BEGIN
    INSERT INTO audit_log (table_name, operation, record_id, old_values, new_values, user_name)
    VALUES (
        'users', 
        'UPDATE', 
        NEW.id,
        JSON_OBJECT('username', OLD.username, 'email', OLD.email),
        JSON_OBJECT('username', NEW.username, 'email', NEW.email),
        USER()
    );
END //
DELIMITER ;

2. 数据验证

sql
-- 订单表
CREATE TABLE orders (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    product_id INT,
    quantity INT,
    unit_price DECIMAL(10,2),
    total_amount DECIMAL(10,2),
    status ENUM('pending', 'confirmed', 'shipped', 'delivered', 'cancelled') DEFAULT 'pending',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 库存表
CREATE TABLE inventory (
    product_id INT PRIMARY KEY,
    stock_quantity INT DEFAULT 0
);

-- 订单插入前验证库存
DELIMITER //
CREATE TRIGGER tr_validate_order_stock
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
    DECLARE available_stock INT DEFAULT 0;
    
    -- 获取可用库存
    SELECT IFNULL(stock_quantity, 0) INTO available_stock 
    FROM inventory 
    WHERE product_id = NEW.product_id;
    
    -- 检查库存是否充足
    IF available_stock < NEW.quantity THEN
        SET @error_message = CONCAT('库存不足,当前库存:', available_stock, ',需要:', NEW.quantity);
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = @error_message;
    END IF;
    
    -- 计算总金额
    SET NEW.total_amount = NEW.unit_price * NEW.quantity;
END //
DELIMITER ;

-- 订单更新前验证
DELIMITER //
CREATE TRIGGER tr_validate_order_update
BEFORE UPDATE ON orders
FOR EACH ROW
BEGIN
    -- 防止已发货订单被修改数量
    IF OLD.status IN ('shipped', 'delivered') AND OLD.quantity != NEW.quantity THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '已发货或已交付订单不能修改数量';
    END IF;
    
    -- 重新计算总金额
    SET NEW.total_amount = NEW.unit_price * NEW.quantity;
END //
DELIMITER ;

3. 自动计算字段

sql
-- 购物车表
CREATE TABLE cart_items (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    product_id INT,
    quantity INT DEFAULT 1,
    unit_price DECIMAL(10,2),
    line_total DECIMAL(10,2)
);

-- 自动计算行总计
DELIMITER //
CREATE TRIGGER tr_calculate_line_total
BEFORE INSERT ON cart_items
FOR EACH ROW
BEGIN
    SET NEW.line_total = NEW.unit_price * NEW.quantity;
END //
DELIMITER ;

DELIMITER //
CREATE TRIGGER tr_update_line_total
BEFORE UPDATE ON cart_items
FOR EACH ROW
BEGIN
    SET NEW.line_total = NEW.unit_price * NEW.quantity;
END //
DELIMITER ;

4. 数据同步

sql
-- 用户统计表
CREATE TABLE user_stats (
    user_id INT PRIMARY KEY,
    total_orders INT DEFAULT 0,
    total_spent DECIMAL(12,2) DEFAULT 0.00,
    last_order_date DATE,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 订单完成后更新用户统计
DELIMITER //
CREATE TRIGGER tr_update_user_stats
AFTER UPDATE ON orders
FOR EACH ROW
BEGIN
    -- 只有当订单状态变为已完成时才更新统计
    IF OLD.status != 'delivered' AND NEW.status = 'delivered' THEN
        INSERT INTO user_stats (user_id, total_orders, total_spent, last_order_date)
        VALUES (NEW.user_id, 1, NEW.total_amount, DATE(NEW.created_at))
        ON DUPLICATE KEY UPDATE
            total_orders = total_orders + 1,
            total_spent = total_spent + NEW.total_amount,
            last_order_date = DATE(NEW.created_at);
    END IF;
    
    -- 如果订单被取消,减少统计(如果订单之前已完成)
    IF OLD.status = 'delivered' AND NEW.status = 'cancelled' THEN
        UPDATE user_stats SET
            total_orders = total_orders - 1,
            total_spent = total_spent - NEW.total_amount
        WHERE user_id = NEW.user_id;
    END IF;
END //
DELIMITER ;

触发器管理

查看触发器

sql
-- 查看所有触发器
SHOW TRIGGERS;

-- 查看特定表的触发器
SHOW TRIGGERS WHERE `Table` = 'users';

-- 查看特定数据库的触发器
SHOW TRIGGERS FROM your_database_name;

-- 从information_schema查看
SELECT 
    TRIGGER_NAME,
    EVENT_MANIPULATION,
    EVENT_OBJECT_TABLE,
    ACTION_TIMING,
    ACTION_STATEMENT
FROM information_schema.TRIGGERS 
WHERE TRIGGER_SCHEMA = 'your_database_name';

修改和删除触发器

sql
-- 删除触发器
DROP TRIGGER IF EXISTS tr_users_insert;

-- 要修改触发器,需要先删除再重新创建
DROP TRIGGER IF EXISTS tr_users_update;
DELIMITER //
CREATE TRIGGER tr_users_update
BEFORE UPDATE ON users
FOR EACH ROW
BEGIN
    SET NEW.updated_at = NOW();
    
    -- 增强的更新逻辑
    IF OLD.email != NEW.email THEN
        INSERT INTO email_change_log (user_id, old_email, new_email, change_time)
        VALUES (NEW.id, OLD.email, NEW.email, NOW());
    END IF;
END //
DELIMITER ;

触发器最佳实践

1. 性能考虑

sql
-- 避免在触发器中执行耗时操作
DELIMITER //
CREATE TRIGGER tr_efficient_trigger
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
    -- 快速操作:只记录必要信息
    INSERT INTO order_stats_queue (order_id, action, created_at) 
    VALUES (NEW.id, 'process', NOW());
    
    -- 复杂处理交给后台任务或存储过程
END //
DELIMITER ;

2. 错误处理

sql
DELIMITER //
CREATE TRIGGER tr_safe_trigger
BEFORE UPDATE ON users
FOR EACH ROW
BEGIN
    -- 数据验证
    IF NEW.email IS NOT NULL AND NEW.email != '' THEN
        IF NEW.email NOT REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$' THEN
            SIGNAL SQLSTATE '45000' 
            SET MESSAGE_TEXT = '邮箱格式无效';
        END IF;
    END IF;
    
    -- 业务规则验证
    IF NEW.username REGEXP '[^a-zA-Z0-9_]' THEN
        SIGNAL SQLSTATE '45000' 
        SET MESSAGE_TEXT = '用户名只能包含字母、数字和下划线';
    END IF;
END //
DELIMITER ;

3. 避免无限循环

sql
-- 错误示例:可能导致无限循环
-- DELIMITER //
-- CREATE TRIGGER tr_bad_loop
-- AFTER UPDATE ON users
-- FOR EACH ROW
-- BEGIN
--     UPDATE users SET updated_at = NOW() WHERE id = NEW.id; -- 这会再次触发此触发器!
-- END //
-- DELIMITER ;

-- 正确示例:避免循环
DELIMITER //
CREATE TRIGGER tr_avoid_loop
AFTER UPDATE ON users
FOR EACH ROW
BEGIN
    -- 只在特定条件下更新,避免无限循环
    IF NEW.status = 'premium' AND OLD.status != 'premium' THEN
        INSERT INTO premium_activation_log (user_id, activated_at) 
        VALUES (NEW.id, NOW());
    END IF;
END //
DELIMITER ;

高级触发器应用

1. 复杂业务规则

sql
-- 创建优惠券表
CREATE TABLE coupons (
    id INT AUTO_INCREMENT PRIMARY KEY,
    code VARCHAR(50) UNIQUE,
    discount_percent DECIMAL(5,2),
    max_uses INT,
    used_count INT DEFAULT 0,
    expires_at DATE
);

-- 订单使用优惠券时验证规则
DELIMITER //
CREATE TRIGGER tr_validate_coupon_use
BEFORE INSERT ON order_coupons
FOR EACH ROW
BEGIN
    DECLARE coupon_valid BOOLEAN DEFAULT FALSE;
    DECLARE remaining_uses INT DEFAULT 0;
    
    -- 检查优惠券是否有效
    SELECT 
        (expires_at >= CURDATE() AND used_count < max_uses) AS is_valid,
        (max_uses - used_count) AS remaining
    INTO coupon_valid, remaining_uses
    FROM coupons 
    WHERE id = NEW.coupon_id;
    
    IF NOT coupon_valid THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '优惠券无效或已过期';
    END IF;
    
    IF remaining_uses <= 0 THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '优惠券使用次数已达上限';
    END IF;
    
    -- 更新优惠券使用次数
    UPDATE coupons SET used_count = used_count + 1 WHERE id = NEW.coupon_id;
END //
DELIMITER ;

2. 数据一致性维护

sql
-- 分类表
CREATE TABLE categories (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100),
    slug VARCHAR(100) UNIQUE,
    product_count INT DEFAULT 0
);

-- 产品表
CREATE TABLE products (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(200),
    category_id INT,
    price DECIMAL(10,2)
);

-- 当产品插入/删除时更新分类的产品数量
DELIMITER //
CREATE TRIGGER tr_update_category_count_insert
AFTER INSERT ON products
FOR EACH ROW
BEGIN
    UPDATE categories SET product_count = product_count + 1 WHERE id = NEW.category_id;
END //
DELIMITER ;

DELIMITER //
CREATE TRIGGER tr_update_category_count_delete
AFTER DELETE ON products
FOR EACH ROW
BEGIN
    UPDATE categories SET product_count = product_count - 1 WHERE id = OLD.category_id;
END //
DELIMITER ;

DELIMITER //
CREATE TRIGGER tr_update_category_count_update
AFTER UPDATE ON products
FOR EACH ROW
BEGIN
    -- 如果分类改变了,更新旧分类和新分类的计数
    IF OLD.category_id != NEW.category_id THEN
        UPDATE categories SET product_count = product_count - 1 WHERE id = OLD.category_id;
        UPDATE categories SET product_count = product_count + 1 WHERE id = NEW.category_id;
    END IF;
END //
DELIMITER ;

触发器的限制和注意事项

1. 限制

  • 不能使用CALL语句调用存储过程中的动态SQL
  • 不能返回结果集
  • 不能使用某些SQL语句(如ALTER TABLE)
  • 在某些情况下可能影响性能

2. 注意事项

sql
-- 考虑使用存储过程替代复杂触发器
DELIMITER //
CREATE PROCEDURE ProcessOrderWithValidation(
    IN p_user_id INT,
    IN p_product_id INT,
    IN p_quantity INT
)
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        RESIGNAL;
    END;
    
    START TRANSACTION;
    
    -- 所有验证和处理都在存储过程中完成
    -- 这样更容易测试和维护
    
    COMMIT;
END //
DELIMITER ;

调试触发器

sql
-- 创建调试日志表
CREATE TABLE trigger_debug_log (
    id INT AUTO_INCREMENT PRIMARY KEY,
    trigger_name VARCHAR(100),
    operation VARCHAR(20),
    record_id INT,
    debug_info TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 在触发器中记录调试信息
DELIMITER //
CREATE TRIGGER tr_debug_example
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
    INSERT INTO trigger_debug_log (trigger_name, operation, record_id, debug_info)
    VALUES ('tr_debug_example', 'INSERT', NEW.id, CONCAT('New order created: ', NEW.id));
END //
DELIMITER ;

总结

MySQL触发器是实现复杂业务逻辑和数据完整性的重要工具。在使用触发器时需要注意:

  1. 性能影响:触发器会增加每个相关操作的时间开销
  2. 调试困难:触发器逻辑隐藏在数据库层,难以调试
  3. 维护复杂性:触发器逻辑分散在数据库中,增加维护难度
  4. 执行顺序:多个触发器的执行顺序需要特别注意
  5. 错误处理:触发器中的错误可能阻止原始操作完成

触发器最适合用于:

  • 数据验证和约束
  • 审计日志记录
  • 自动计算字段
  • 数据同步和维护

在设计触发器时,应该权衡其便利性和潜在的复杂性,确保它们确实增加了价值而不是引入了不必要的复杂度。