跳到主要内容

MySQL 触发器

触发器是一种特殊的存储过程,当表上发生特定事件时自动执行。本章将详细介绍触发器的创建和使用。

什么是触发器?

触发器(Trigger)是与表关联的数据库对象,当对表执行 INSERT、UPDATE 或 DELETE 操作时,触发器会自动执行。触发器是预先定义的 SQL 语句集合,在特定事件发生时自动激活。

触发器的用途

  • 数据验证:在插入或更新前验证数据
  • 审计日志:记录数据的变更历史
  • 级联更新:更新相关表的数据
  • 数据同步:保持多个表的数据一致
  • 业务规则:实现复杂的业务约束

触发器的优缺点

优点

  • 自动执行,无需手动调用
  • 保证数据一致性
  • 可以实现复杂的业务规则

缺点

  • 隐藏了业务逻辑,难以调试
  • 增加数据库负担
  • 可能导致性能问题
  • 不易于移植

创建触发器

基本语法

CREATE TRIGGER 触发器名
{BEFORE | AFTER} {INSERT | UPDATE | DELETE}
ON 表名
FOR EACH ROW
[{FOLLOWS | PRECEDES} 其他触发器名]
触发器体;

完整语法:

CREATE [DEFINER = user] TRIGGER [IF NOT EXISTS] 触发器名
触发时机
触发事件
ON 表名
FOR EACH ROW
[触发器顺序]
触发器体

触发时机

  • BEFORE:在事件发生之前执行(可以在操作前修改数据或阻止操作)
  • AFTER:在事件发生之后执行(适合记录日志或级联操作)

触发事件

事件说明激活的语句
INSERT插入数据时触发INSERT, LOAD DATA, REPLACE
UPDATE更新数据时触发UPDATE
DELETE删除数据时触发DELETE, REPLACE

注意:DROP TABLE 和 TRUNCATE TABLE 不会激活 DELETE 触发器。

INSERT 触发器

BEFORE INSERT

在插入数据前执行,可以修改要插入的数据:

DELIMITER //

CREATE TRIGGER before_user_insert
BEFORE INSERT ON users
FOR EACH ROW
BEGIN
-- 自动生成创建时间
IF NEW.created_at IS NULL THEN
SET NEW.created_at = NOW();
END IF;

-- 验证邮箱格式
IF NEW.email NOT LIKE '%@%.%' THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '邮箱格式不正确';
END IF;

-- 密码加密
SET NEW.password = MD5(NEW.password);
END //

DELIMITER ;

-- 测试
INSERT INTO users (name, email, password) VALUES ('张三', '[email protected]', '123456');
-- 密码会被自动加密,created_at 会被自动设置

NEW 关键字:在 INSERT 触发器中,NEW 代表将要插入的新行。在 BEFORE INSERT 中可以修改 NEW 的值。

AFTER INSERT

在插入数据后执行,适合记录日志:

DELIMITER //

CREATE TRIGGER after_order_insert
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
-- 记录订单日志
INSERT INTO order_logs (order_id, action, action_time, user_id)
VALUES (NEW.id, 'CREATE', NOW(), NEW.user_id);

-- 更新用户订单统计
UPDATE users
SET order_count = order_count + 1,
total_spent = total_spent + NEW.total
WHERE id = NEW.user_id;
END //

DELIMITER ;

UPDATE 触发器

BEFORE UPDATE

在更新数据前执行,可以验证或修改数据:

DELIMITER //

CREATE TRIGGER before_product_update
BEFORE UPDATE ON products
FOR EACH ROW
BEGIN
-- 价格不能降低超过 50%
IF NEW.price < OLD.price * 0.5 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '降价幅度不能超过 50%';
END IF;

-- 记录价格变更
IF NEW.price != OLD.price THEN
SET NEW.price_updated_at = NOW();
END IF;

-- 自动更新修改时间
SET NEW.updated_at = NOW();
END //

DELIMITER ;

OLD 与 NEW 关键字

  • OLD:更新前的旧值(只读)
  • NEW:更新后的新值(BEFORE 中可读写)

AFTER UPDATE

在更新数据后执行,适合记录变更历史:

DELIMITER //

CREATE TRIGGER after_user_update
AFTER UPDATE ON users
FOR EACH ROW
BEGIN
-- 记录用户信息变更
IF OLD.name != NEW.name OR OLD.email != NEW.email THEN
INSERT INTO user_change_logs (
user_id,
old_name, new_name,
old_email, new_email,
changed_at
) VALUES (
NEW.id,
OLD.name, NEW.name,
OLD.email, NEW.email,
NOW()
);
END IF;
END //

DELIMITER ;

DELETE 触发器

BEFORE DELETE

在删除数据前执行,可以执行级联检查:

DELIMITER //

CREATE TRIGGER before_user_delete
BEFORE DELETE ON users
FOR EACH ROW
BEGIN
-- 检查用户是否有订单
IF EXISTS (SELECT 1 FROM orders WHERE user_id = OLD.id) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '该用户有订单记录,不能删除';
END IF;
END //

DELIMITER ;

OLD 关键字:在 DELETE 触发器中,OLD 代表将要删除的行(只读)。

AFTER DELETE

在删除数据后执行,适合记录删除日志或清理关联数据:

DELIMITER //

CREATE TRIGGER after_order_delete
AFTER DELETE ON orders
FOR EACH ROW
BEGIN
-- 记录删除日志
INSERT INTO order_logs (order_id, action, action_time)
VALUES (OLD.id, 'DELETE', NOW());

-- 恢复库存
UPDATE products p
JOIN order_items oi ON oi.order_id = OLD.id AND oi.product_id = p.id
SET p.stock = p.stock + oi.quantity;

-- 更新用户统计
UPDATE users
SET order_count = order_count - 1,
total_spent = total_spent - OLD.total
WHERE id = OLD.user_id;
END //

DELIMITER ;

触发器中的 NEW 与 OLD

触发事件NEWOLD
INSERT可读可写(BEFORE)不可用
UPDATE可读可写(BEFORE)只读
DELETE不可用只读

使用示例

-- 示例:记录所有数据变更
DELIMITER //

CREATE TRIGGER after_product_change
AFTER UPDATE ON products
FOR EACH ROW
BEGIN
INSERT INTO product_audit_log (
product_id,
field_name,
old_value,
new_value,
changed_at
) VALUES
(NEW.id, 'name', OLD.name, NEW.name, NOW()),
(NEW.id, 'price', OLD.price, NEW.price, NOW()),
(NEW.id, 'stock', OLD.stock, NEW.stock, NOW());
END //

DELIMITER ;

查看和管理触发器

查看触发器

-- 查看数据库中的触发器
SHOW TRIGGERS;

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

-- 查看触发器定义
SHOW CREATE TRIGGER trigger_name;

-- 从信息模式查询
SELECT
TRIGGER_NAME,
EVENT_MANIPULATION,
EVENT_OBJECT_TABLE,
ACTION_TIMING
FROM INFORMATION_SCHEMA.TRIGGERS
WHERE TRIGGER_SCHEMA = 'your_database';

删除触发器

-- 删除触发器
DROP TRIGGER IF EXISTS trigger_name;

禁用和启用触发器

MySQL 不支持直接禁用触发器,但可以通过以下方式实现:

-- 方式 1:删除触发器
DROP TRIGGER IF EXISTS trigger_name;

-- 方式 2:使用条件判断
DELIMITER //

CREATE TRIGGER conditional_trigger
BEFORE INSERT ON users
FOR EACH ROW
BEGIN
-- 通过变量控制是否执行
IF @disable_triggers IS NULL OR @disable_triggers = 0 THEN
-- 触发器逻辑
SET NEW.created_at = NOW();
END IF;
END //

DELIMITER ;

-- 禁用触发器
SET @disable_triggers = 1;
-- 执行操作...
INSERT INTO users (name) VALUES ('测试');

-- 启用触发器
SET @disable_triggers = 0;

触发器实战案例

案例 1:数据审计

-- 创建审计表
CREATE TABLE employee_audit (
id INT AUTO_INCREMENT PRIMARY KEY,
employee_id INT,
action VARCHAR(10),
old_data JSON,
new_data JSON,
changed_by VARCHAR(50),
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 创建触发器
DELIMITER //

CREATE TRIGGER employee_audit_insert
AFTER INSERT ON employees
FOR EACH ROW
BEGIN
INSERT INTO employee_audit (employee_id, action, new_data, changed_by)
VALUES (
NEW.id,
'INSERT',
JSON_OBJECT('name', NEW.name, 'department', NEW.department, 'salary', NEW.salary),
CURRENT_USER()
);
END //

CREATE TRIGGER employee_audit_update
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
INSERT INTO employee_audit (employee_id, action, old_data, new_data, changed_by)
VALUES (
NEW.id,
'UPDATE',
JSON_OBJECT('name', OLD.name, 'department', OLD.department, 'salary', OLD.salary),
JSON_OBJECT('name', NEW.name, 'department', NEW.department, 'salary', NEW.salary),
CURRENT_USER()
);
END //

CREATE TRIGGER employee_audit_delete
AFTER DELETE ON employees
FOR EACH ROW
BEGIN
INSERT INTO employee_audit (employee_id, action, old_data, changed_by)
VALUES (
OLD.id,
'DELETE',
JSON_OBJECT('name', OLD.name, 'department', OLD.department, 'salary', OLD.salary),
CURRENT_USER()
);
END //

DELIMITER ;

案例 2:库存管理

-- 订单明细表插入后减少库存
DELIMITER //

CREATE TRIGGER after_order_item_insert
AFTER INSERT ON order_items
FOR EACH ROW
BEGIN
UPDATE products
SET stock = stock - NEW.quantity,
updated_at = NOW()
WHERE id = NEW.product_id;

-- 检查库存是否不足
IF (SELECT stock FROM products WHERE id = NEW.product_id) < 10 THEN
INSERT INTO low_stock_alerts (product_id, current_stock, alert_time)
VALUES (NEW.product_id,
(SELECT stock FROM products WHERE id = NEW.product_id),
NOW());
END IF;
END //

-- 订单取消后恢复库存
CREATE TRIGGER after_order_item_delete
AFTER DELETE ON order_items
FOR EACH ROW
BEGIN
UPDATE products
SET stock = stock + OLD.quantity,
updated_at = NOW()
WHERE id = OLD.product_id;
END //

DELIMITER ;

案例 3:数据同步

-- 同步用户状态到缓存表
DELIMITER //

CREATE TRIGGER sync_user_status
AFTER UPDATE ON users
FOR EACH ROW
BEGIN
IF OLD.status != NEW.status THEN
INSERT INTO user_cache (user_id, status, updated_at)
VALUES (NEW.id, NEW.status, NOW())
ON DUPLICATE KEY UPDATE
status = NEW.status,
updated_at = NOW();
END IF;
END //

DELIMITER ;

案例 4:计算字段自动更新

-- 订单明细变更后更新订单总额
DELIMITER //

CREATE TRIGGER update_order_total_after_item_change
AFTER INSERT ON order_items
FOR EACH ROW
BEGIN
UPDATE orders o
SET total = (
SELECT COALESCE(SUM(price * quantity), 0)
FROM order_items
WHERE order_id = NEW.order_id
)
WHERE id = NEW.order_id;
END //

DELIMITER ;

触发器最佳实践

1. 避免递归触发

-- 危险:可能导致无限循环
CREATE TRIGGER update_user_balance
AFTER UPDATE ON users
FOR EACH ROW
BEGIN
-- 这会再次触发 UPDATE,造成无限循环!
UPDATE users SET last_modified = NOW() WHERE id = NEW.id;
END;

-- 解决方案:使用 BEFORE 触发器
CREATE TRIGGER update_user_timestamp
BEFORE UPDATE ON users
FOR EACH ROW
BEGIN
SET NEW.last_modified = NOW();
END;

2. 保持触发器简单

-- 不好:触发器中包含复杂逻辑
CREATE TRIGGER complex_trigger
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
-- 大量复杂的业务逻辑...
-- 难以调试和维护
END;

-- 好:触发器调用存储过程
CREATE TRIGGER process_order_trigger
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
CALL process_order(NEW.id);
END;

3. 错误处理

DELIMITER //

CREATE TRIGGER safe_insert_trigger
BEFORE INSERT ON critical_table
FOR EACH ROW
BEGIN
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
BEGIN
-- 记录错误但不阻止插入
INSERT INTO error_logs (error_message, created_at)
VALUES (CONCAT('Trigger error for table critical_table'), NOW());
END;

-- 触发器逻辑
IF NEW.value < 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Value cannot be negative';
END IF;
END //

DELIMITER ;

4. 性能考虑

-- 触发器会增加操作的开销
-- 对于批量操作,考虑使用存储过程代替

-- 不好:每行都触发
CREATE TRIGGER log_every_insert
AFTER INSERT ON large_table
FOR EACH ROW
BEGIN
INSERT INTO logs (message) VALUES ('Row inserted');
END;

-- 好:在应用层批量处理,或使用定时任务

MySQL 8.0 触发器新特性

多触发器支持

MySQL 8.0 允许同一事件有多个触发器:

-- 第一个触发器
CREATE TRIGGER before_user_insert_1
BEFORE INSERT ON users
FOR EACH ROW
BEGIN
SET NEW.created_at = NOW();
END;

-- 第二个触发器(MySQL 8.0+)
CREATE TRIGGER before_user_insert_2
BEFORE INSERT ON users
FOR EACH ROW FOLLOWS before_user_insert_1
BEGIN
SET NEW.updated_at = NOW();
END;

-- 使用 PRECEDES 或 FOLLOWS 指定执行顺序

触发器的限制

  1. 不能为临时表或视图创建触发器
  2. 触发器不能使用 CALL 语句调用返回结果集的存储过程
  3. 触发器不能使用动态 SQL(PREPARE、EXECUTE)
  4. 触发器不能使用显式或隐式的事务语句(BEGIN、COMMIT、ROLLBACK)
  5. 外键级联操作不会激活触发器
  6. 触发器中不能引用生成列的 OLD 或 NEW 值

小结

本章我们学习了:

  1. 触发器概念:自动执行的存储过程
  2. 触发时机:BEFORE 和 AFTER
  3. 触发事件:INSERT、UPDATE、DELETE
  4. NEW 与 OLD:访问新值和旧值
  5. 触发器管理:查看、创建、删除
  6. 实战案例:审计、库存管理、数据同步
  7. 最佳实践:避免递归、保持简单、性能考虑

练习

  1. 创建一个触发器,在用户注册时自动设置创建时间
  2. 创建一个触发器,记录产品价格变更历史
  3. 创建一个触发器,防止删除有订单的用户
  4. 实现一个完整的审计系统,记录所有表的变更

参考资料