[代码重构](master): 触发器

2022年11月16日14:07:06
master
土豆兄弟 2 years ago
parent f67f610453
commit d474422cc8

@ -1228,6 +1228,312 @@ mysql> select * from demo.goodsmaster; -- 查看商品信息表,平均进价
- 有个小问题要提醒你注意:**如果一个操作要用到另外一个操作的结果,那我们一定不能搞错操作的顺序**。
### 2.4 触发器:如何让数据修改自动触发关联操作,确保数据一致性?
- 在实际开发中,我们经常会遇到这样的情况:有 2 个或者多个相互关联的表,如商品信息和库存信息分别存放在 2 个不同的数据表中,我们在添加一条新商品记录的时候,为了保
证数据的完整性,必须同时在库存表中添加一条库存记录。
- 这样一来,我们就必须把这两个关联的操作步骤写到程序里面,而且要用事务包裹起来,确保这两个操作成为一个原子操作,要么全部执行,要么全部不执行。要是遇到特殊情
况,可能还需要对数据进行手动维护,这样就很容易忘记其中的一步,导致数据缺失。
- 这个时候,其实咱们可以使用触发器。你可以创建一个触发器,让商品信息数据的插入操作自动触发库存数据的插入操作。
- 如何操作触发器?
- 创建触发器
- 创建触发器的语法结构是:
```sql
CREATE TRIGGER 触发器名称 {BEFORE|AFTER} {INSERT|UPDATE|DELETE}
ON 表名 FOR EACH ROW 表达式;
```
- 在创建时,你一定要注意触发器的三个要素。
- 表名:表示触发器监控的对象。
- INSERT|UPDATE|DELETE表示触发的事件。INSERT 表示插入记录时触发UPDATE 表示更新记录时触发DELETE 表示删除记录时触发。
- BEFORE|AFTER表示触发的时间。BEFORE 表示在事件之前触发AFTER 表示在事件之后触发。
- 咱们还要知道触发器是不是创建成功了。怎么查看呢?
- 查看触发器
- 查看触发器的语句是:
```sql
SHOW TRIGGERS\G;
```
- 删除触发器
- 删除触发器很简单,你只要知道语法结构就可以了:
```sql
DROP TRIGGER 触发器名称;
```
- 案例
- 超市项目实际实施过程中,客户经常要查询储值余额变动的明细,但是,查询会员消费流水时,存在数据汇总不及时、查询速度比较慢的问题。这时,我们就想到用触发器,及时
把会员储值金额的变化信息记录到一个专门的表中。
- 我先用咱们熟悉的 SQL 语句来实现记录储值金额变动的操作,后面再带你使用触发器来操作。通过两种操作的对比,你就能更好地理解,在什么情况下,触发器能够比普通的 SQL
语句更加简洁高效,从而帮助你用好触发器这个工具,提高开发的能力。
- 下面我就借助具体数据来详细说明一下。这里我们需要用到会员信息表demo.membermaster和会员储值历史表demo.deposithist
- 会员信息表:
- ![触发器会员信息表](pic/触发器会员信息表.png)
- 会员储值历史表:
- ![会员储值历史表](pic/会员储值历史表.png)
- 假如在 2020 年 12 月 20 日这一天,会员编号是 2 的会员李四,到超市的某家连锁店购买了一条烟,消费了 150 元。现在,我们用之前学过的 SQL 语句,把这个会员储值余额的
变动情况记录到会员储值历史表中。
- 第一步,查询出编号是 2 的会员卡的储值金额是多少。我们可以用下面的代码来实现:
```sql
mysql> SELECT memberdeposit
-> FROM demo.membermaster
-> WHERE memberid = 2;
+---------------+
| memberdeposit |
+---------------+
| 200.00 |
+---------------+
1 row in set (0.00 sec)
```
- 第二步,我们把会员编号是 2 的会员的储值金额减去 150。
```sql
mysql> UPDATE demo.membermaster
-> SET memberdeposit = memberdeposit - 150
-> WHERE memberid = 2;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
- 第三步,读出会员编号是 2 的会员当前的储值金额。
```sql
mysql> SELECT memberdeposit
-> FROM demo.membermaster
-> WHERE memberid = 2;
+---------------+
| memberdeposit |
+---------------+
| 50.00 |
+---------------+
1 row in set (0.00 sec)
```
- 第四步,把会员编号和前面查询中获得的储值起始金额、储值余额和储值金额变化值,写入会员储值历史表。
```sql
mysql> INSERT INTO demo.deposithist
-> (
-> memberid,
-> transdate,
-> oldvalue,
-> newvalue,
-> changedvalue
-> )
-> SELECT 2,NOW(),200,50,-150;
Query OK, 1 row affected (0.02 sec)
Records: 1 Duplicates: 0 Warnings: 0
```
- 这样,我们就完成了记录会员储值金额变动的操作。现在,我们来查询一下记录的结果:
```sql
mysql> SELECT *
-> FROM demo.deposithist;
+----+----------+---------------------+----------+----------+--------------+
| id | memberid | transdate | oldvalue | newvalue | changedvalue |
+----+----------+---------------------+----------+----------+--------------+
| 1 | 2 | 2020-12-20 10:37:51 | 200.00 | 50.00 | -150.00 |
+----+----------+---------------------+----------+----------+--------------+
1 row in set (0.00 sec)
```
- 结果显示,会员编号是 2 的会员卡储值金额在 2020-12-20 10:37:51 时有变动,变动前是 200 元,变动后是 50 元,减少了 150 元。
- 你看这个记录会员储值金额变动的操作非常复杂,我们用了 4 步才完成。而且为了确保数据的一致性,我们还要用事务把这几个关联的操作包裹起来,这样一来,消耗的资源就比较多。
- 那如果用触发器来实现,效果会怎样呢?我们来实操一下。
- 首先,我们创建一个数据表 demo.membermaster 的触发器。每当更新表中的数据时,先触发触发器,如果发现会员储值金额有变化,就把会员编号信息、更新的时间、更新前
的储值金额、更新后的储值金额和变动金额,写入会员储值历史表。然后,再执行会员信息表的更新操作。
- 创建触发器的代码如下所示:
```sql
DELIMITER //
CREATE TRIGGER demo.upd_membermaster BEFORE UPDATE -- 在更新前触发
ON demo.membermaster
FOR EACH ROW -- 表示每更新一条记录,触发一次
BEGIN -- 开始程序体
IF (new.memberdeposit <> old.memberdeposit) -- 如果储值金额有变化
THEN
INSERT INTO demo.deposithist
(
memberid,
transdate,
oldvalue,
newvalue,
changedvalue
)
SELECT
NEW.memberid,
NOW(),
OLD.memberdeposit, -- 更新前的储值金额
NEW.memberdeposit, -- 更新后的储值金额
NEW.memberdeposit-OLD.memberdeposit; -- 储值金额变化值
END IF;
END
//
DELIMITER ;
```
- 创建完成之后,我们查看一下触发器,看看是不是真的创建成功了:
```sql
mysql> SHOW TRIGGERS \G;
*************************** 1. row ***************************
Trigger: upd_membermaster -- 触发器名称
Event: UPDATE -- 触发的事件
Table: membermaster -- 表名称
Statement: BEGIN -- 被触发时要执行的程序体
IF (new.memberdeposit <> old.memberdeposit) -- 储值金额变动时插入历史储值表
THEN
INSERT INTO demo.deposithist
(
memberid,
transdate,
oldvalue,
newvalue,
changedvalue
)
SELECT
NEW.memberid,
NOW(),
OLD.memberdeposit,
NEW.memberdeposit,
NEW.memberdeposit-OLD.memberdeposit;
END IF;
END
Timing: BEFORE
Created: 2021-04-03 15:02:48.18
sql_mode: STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION
Definer: root@localhost
character_set_client: utf8mb4
collation_connection: utf8mb4_0900_ai_ci
Database Collation: utf8mb4_0900_ai_ci
1 row in set (0.00 sec)
```
- 从代码中,我们可以知道触发器的具体信息:
- Trigger 表示触发器名称,这里是 upd_membermaster
- Event 表示触发事件,这里是 UPDATE表示更新触发
- Table 表示定义触发器的数据表,这里是 membermaster
- Statement 表示触发时要执行的程序体。
- 看到这些信息,我们就可以确认,触发器创建成功了。
- 创建成功以后,我们尝试更新一下会员编号是 1 的会员的储值金额,这里假设把会员 1 的储值金额增加 10 元。简单说明一下,会员也可以把钱存到会员卡里,需要的时候进行消
费,对应的就是储值金额的增加。
- 我们用下面的代码来完成这个操作:
```sql
mysql> UPDATE demo.membermaster
-> SET memberdeposit = memberdeposit + 10
-> WHERE memberid = 1;
Query OK, 1 row affected (0.03 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
- 现在,我们来查看一下会员信息表和会员储值历史表:
```sql
mysql> SELECT *
-> FROM demo.membermaster;
+----------+----------+------------+---------+-------------+---------------+
| memberid | cardno | membername | address | phone | memberdeposit |
+----------+----------+------------+---------+-------------+---------------+
| 1 | 10000001 | 张三 | 北京 | 13812345678 | 110.00 |
| 2 | 10000002 | 李四 | 天津 | 18512345678 | 50.00 |
+----------+----------+------------+---------+-------------+---------------+
2 rows in set (0.00 sec)
mysql> SELECT *
-> FROM demo.deposithist;
+----+----------+---------------------+----------+----------+--------------+
| id | memberid | transdate | oldvalue | newvalue | changedvalue |
+----+----------+---------------------+----------+----------+--------------+
| 1 | 2 | 2020-12-20 10:37:51 | 200.00 | 50.00 | -150.00 |
| 2 | 1 | 2020-12-20 11:32:09 | 100.00 | 110.00 | 10.00 |
+----+----------+---------------------+----------+----------+--------------+
2 rows in set (0.01 sec)
```
- 结果显示,触发器正确地记录了修改会员储值金额的操作。
- 如果你一直跟着我进行操作,到这里,你可能会有疑问,在会员历史储值表中记录修改会员储值金额的操作,和实际修改会员信息表是 2 个操作,有没有可能一个成功,一个失
败?比如说,记录修改会员储值金额的操作失败了,但是会员的储值金额被修改了。
- 其实,在 MySQL 中,如果触发器中的操作失败了,那么触发这个触发器的数据操作也会失败,**不会出现一个成功、一个失败的情况**。
- 我还是借助一个例子来解释下。假设我们创建了一个触发器这个触发器的程序体中的SQL 语句有误,比如多了一个字段。在这种情况下,触发器执行会失败,因此,数据更新
的操作也不会执行。我们用下面的代码来验证一下:
```sql
DELIMITER //
CREATE TRIGGER demo.upd_membermaster BEFORE UPDATE
ON demo.membermaster
FOR EACH ROW
BEGIN
IF (new.memberdeposit <> old.memberdeposit)
THEN
INSERT INTO demo.deposithist
(
aa, -- 不存在的字段
memberid,
transdate,
oldvalue,
newvalue,
changedvalue
)
SELECT
1, -- 给不存在的字段赋值
NEW.memberid,
NOW(),
OLD.memberdeposit,
NEW.memberdeposit,
NEW.memberdeposit-OLD.memberdeposit;
END IF;
END
//
DELIMITER ;
```
- 现在,假设我们要把会员编号是 2 的会员卡的储值金额更新为 20
```sql
mysql> update demo.membermaster set memberdeposit=20 where memberid = 2;
ERROR 1054 (42S22): Unknown column 'aa' in 'field list'
```
- 系统提示因为字段“aa”不存在导致触发器执行失败。现在我们来看看会员储值金额有没有被修改
```sql
mysql> select * from demo.membermaster;
+----------+----------+------------+---------+-------------+---------------+
| memberid | cardno | membername | address | phone | memberdeposit |
+----------+----------+------------+---------+-------------+---------------+
| 1 | 10000001 | 张三 | 北京 | 13812345678 | 110.00 |
| 2 | 10000002 | 李四 | 天津 | 18512345678 | 50.00 |
+----------+----------+------------+---------+-------------+---------------+
2 rows in set (0.00 sec)
```
- 结果显示,会员储值金额不变。这个时候,为了让应用程序知道触发器是否执行成功,我们可以通过 ROW_COUNT() 函数来发现错误:
```sql
mysql> select row_count();
+-------------+
| row_count() |
+-------------+
| -1 |
+-------------+
1 row in set (0.00 sec)
```
- 结果是 -1说明我们可以通过 ROW_COUNT() 函数捕获到错误。
- 对于记录会员储值金额变化的操作,可以通过应用层发出 SQL 语句指令,或者用一个存储过程来实现。无论哪种方式,都要通过好几步相互关联的操作,而且要做
成一个事务,处理过程复杂,消耗的资源也较多。如果用触发器,效率就会提高很多,消耗的资源也少。同时,还可以起到事务的类似功能,保证关联操作全部完成或全部失败。
- 触发器的优缺点
- 通过刚刚的案例,你应该感受到触发器的高效了。现在,咱们把视角拔高一下,来看看触发器具体都有什么优缺点。毕竟,知己知彼,才能百战不殆。只有非常清楚它的优缺点,
你才能充分发挥它的作用。
- 我先来说说触发器的优点。
- **首先,触发器可以确保数据的完整性**。
- 每当我们录入、删除和修改一条进货单明细数据的时候,进货单明细表里的数据就会发生变动。这个时候,在进货单头表中的总计数量和总计金额就必须重新计算,否则,进货单
头表中的总计数量和总计金额就不等于进货单明细表中数量合计和金额合计了,这就是数据不一致。
- 为了解决这个问题,我们就可以使用触发器,规定每当进货单明细表有数据插入、修改和删除的操作时,自动触发 2 步操作:
- 重新计算进货单明细表中的数量合计和金额合计;
- 用第一步中计算出来的值更新进货单头表中的合计数量与合计金额。
- 这样一来,进货单头表中的合计数量与合计金额的值,就始终与进货单明细表中计算出来的合计数量与合计金额的值相同,数据就是一致的,不会互相矛盾。
- **其次,触发器可以帮助我们记录操作日志**。
- 利用触发器,可以具体记录什么时间发生了什么。我们前面的记录修改会员储值金额的触发器,就是一个很好的例子。这对我们还原操作执行时的具体场景,更好地定位问题原因
很有帮助。
- **另外,触发器还可以用在操作数据前,对数据进行合法性检查。**
- 举个小例子。超市进货的时候,需要库管录入进货价格。但是,人为操作很容易犯错误,比如说在录入数量的时候,把条形码扫进去了;录入金额的时候,看串了行,录入的价格
远超售价,导致账面上的巨亏……这些都可以通过触发器,在实际插入或者更新操作之前,对相应的数据进行检查,及时提示错误,防止错误数据进入系统。
- 下面我来说说触发器的缺点。
- **触发器最大的一个问题就是可读性差**。
- 因为触发器存储在数据库中,并且由事件驱动,这就意味着触发器有可能不受应用层的控制。这对系统维护是非常有挑战的。
- 还是拿我们创建触发器时讲到的修改会员储值操作的那个触发器为例。如果触发器中的操作出了问题,会导致会员储值金额更新失败。我用下面的代码演示一下:
```sql
mysql> update demo.membermaster set memberdeposit=20 where memberid = 2;
ERROR 1054 (42S22): Unknown column 'aa' in 'field list'
```
- 结果显示系统提示错误字段“aa”不存在。
- 这是因为,触发器中的数据插入操作多了一个字段,系统提示错误。可是,如果你不了解这个触发器,很可能会认为是更新语句本身的问题,或者是会员信息表的结构出了问题。
说不定你还会给会员信息表添加一个叫“aa”的字段试图解决这个问题结果只能是白费力。
- 另外,相关数据的变更,特别是数据表结构的变更,都可能会导致触发器出错,进而影响数据操作的正常运行。这些都会由于触发器本身的隐蔽性,影响到应用中错误原因排查的
效率。
- 小建议:**维护一个完整的数据库设计文档**。因为运维人员可能会经常变动,如果有一个完整的数据库设计文档,就可以帮助新人快速了解触发器的设计
思路,从而减少错误,降低系统维护的成本。
### 2.5 权限管理:如何控制数据库访问,消除安全隐患?

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Loading…
Cancel
Save