CategoryResourceRepost/极客时间专栏/MySQL 必知必会/进阶篇/17 | 触发器:如何让数据修改自动触发关联操作,确保数据一致性?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

432 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="17 | 触发器:如何让数据修改自动触发关联操作,确保数据一致性?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/97/81/97d0bcc17f014yy02b4e5416ff27e981.mp3"></audio>
你好,我是朱晓峰。今天,我来和你聊一聊触发器。
在实际开发中我们经常会遇到这样的情况有2个或者多个相互关联的表如商品信息和库存信息分别存放在2个不同的数据表中我们在添加一条新商品记录的时候为了保证数据的完整性必须同时在库存表中添加一条库存记录。
这样一来,我们就必须把这两个关联的操作步骤写到程序里面,而且要用事务包裹起来,确保这两个操作成为一个原子操作,要么全部执行,要么全部不执行。要是遇到特殊情况,可能还需要对数据进行手动维护,这样就很容易忘记其中的一步,导致数据缺失。
这个时候,其实咱们可以使用触发器。你可以创建一个触发器,让商品信息数据的插入操作自动触发库存数据的插入操作。这样一来,就不用担心因为忘记添加库存数据而导致的数据缺失了。
听上去好像很不错,那触发器到底怎么使用呢?接下来,我就重点给你聊聊。我会先给你讲解创建、查看和删除触发器的具体操作,然后借助一个案例带你实战一下。
## 如何操作触发器?
首先,咱们来学习下触发器的基本操作。
### 创建触发器
创建触发器的语法结构是:
```
CREATE TRIGGER 触发器名称 {BEFORE|AFTER} {INSERT|UPDATE|DELETE}
ON 表名 FOR EACH ROW 表达式;
```
在创建时,你一定要注意触发器的三个要素。
- 表名:表示触发器监控的对象。
- INSERT|UPDATE|DELETE表示触发的事件。INSERT表示插入记录时触发UPDATE表示更新记录时触发DELETE表示删除记录时触发。
- BEFORE|AFTER表示触发的时间。BEFORE表示在事件之前触发AFTER表示在事件之后触发。
只有把这三个要素定义好,才能正确使用触发器。
创建好触发器之后,咱们还要知道触发器是不是创建成功了。怎么查看呢?我来介绍下。
### 查看触发器
查看触发器的语句是:
```
SHOW TRIGGERS\G;
```
### 删除触发器
删除触发器很简单,你只要知道语法结构就可以了:
```
DROP TRIGGER 触发器名称;
```
知道了触发器的操作方法,接下来咱们就借助超市项目的实际案例,在真实的场景中实战一下,毕竟,实战是掌握操作的最好方法。
## 案例讲解
超市项目实际实施过程中,客户经常要查询储值余额变动的明细,但是,查询会员消费流水时,存在数据汇总不及时、查询速度比较慢的问题。这时,我们就想到用触发器,及时把会员储值金额的变化信息记录到一个专门的表中。
我先用咱们熟悉的SQL语句来实现记录储值金额变动的操作后面再带你使用触发器来操作。通过两种操作的对比你就能更好地理解在什么情况下触发器能够比普通的SQL语句更加简洁高效从而帮助你用好触发器这个工具提高开发的能力。
下面我就借助具体数据来详细说明一下。这里我们需要用到会员信息表demo.membermaster和会员储值历史表demo.deposithist
会员信息表:
<img src="https://static001.geekbang.org/resource/image/26/34/26166082e21c2cbd38e4c21d03e76734.jpeg" alt="">
会员储值历史表:
<img src="https://static001.geekbang.org/resource/image/f0/c5/f0b59f65801543af6c52b208c71925c5.jpeg" alt="">
假如在2020年12月20日这一天会员编号是2的会员李四到超市的某家连锁店购买了一条烟消费了150元。现在我们用之前学过的SQL语句把这个会员储值余额的变动情况记录到会员储值历史表中。
第一步查询出编号是2的会员卡的储值金额是多少。我们可以用下面的代码来实现
```
mysql&gt; SELECT memberdeposit
-&gt; FROM demo.membermaster
-&gt; WHERE memberid = 2;
+---------------+
| memberdeposit |
+---------------+
| 200.00 |
+---------------+
1 row in set (0.00 sec)
```
第二步我们把会员编号是2的会员的储值金额减去150。
```
mysql&gt; UPDATE demo.membermaster
-&gt; SET memberdeposit = memberdeposit - 150
-&gt; WHERE memberid = 2;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
第三步读出会员编号是2的会员当前的储值金额。
```
mysql&gt; SELECT memberdeposit
-&gt; FROM demo.membermaster
-&gt; WHERE memberid = 2;
+---------------+
| memberdeposit |
+---------------+
| 50.00 |
+---------------+
1 row in set (0.00 sec)
```
第四步,把会员编号和前面查询中获得的储值起始金额、储值余额和储值金额变化值,写入会员储值历史表。
```
mysql&gt; INSERT INTO demo.deposithist
-&gt; (
-&gt; memberid,
-&gt; transdate,
-&gt; oldvalue,
-&gt; newvalue,
-&gt; changedvalue
-&gt; )
-&gt; SELECT 2,NOW(),200,50,-150;
Query OK, 1 row affected (0.02 sec)
Records: 1 Duplicates: 0 Warnings: 0
```
这样,我们就完成了记录会员储值金额变动的操作。现在,我们来查询一下记录的结果:
```
mysql&gt; SELECT *
-&gt; 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的触发器。每当更新表中的数据时先触发触发器如果发现会员储值金额有变化就把会员编号信息、更新的时间、更新前的储值金额、更新后的储值金额和变动金额写入会员储值历史表。然后再执行会员信息表的更新操作。
创建触发器的代码如下所示:
```
DELIMITER //
CREATE TRIGGER demo.upd_membermaster BEFORE UPDATE -- 在更新前触发
ON demo.membermaster
FOR EACH ROW -- 表示每更新一条记录,触发一次
BEGIN -- 开始程序体
IF (new.memberdeposit &lt;&gt; 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 ;
```
创建完成之后,我们查看一下触发器,看看是不是真的创建成功了:
```
mysql&gt; SHOW TRIGGERS \G;
*************************** 1. row ***************************
Trigger: upd_membermaster -- 触发器名称
Event: UPDATE -- 触发的事件
Table: membermaster -- 表名称
Statement: BEGIN -- 被触发时要执行的程序体
IF (new.memberdeposit &lt;&gt; 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元。简单说明一下会员也可以把钱存到会员卡里需要的时候进行消费对应的就是储值金额的增加。
我们用下面的代码来完成这个操作:
```
mysql&gt; UPDATE demo.membermaster
-&gt; SET memberdeposit = memberdeposit + 10
-&gt; WHERE memberid = 1;
Query OK, 1 row affected (0.03 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
现在,我们来查看一下会员信息表和会员储值历史表:
```
mysql&gt; SELECT *
-&gt; 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&gt; SELECT *
-&gt; 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语句有误比如多了一个字段。在这种情况下触发器执行会失败因此数据更新的操作也不会执行。我们用下面的代码来验证一下
```
DELIMITER //
CREATE TRIGGER demo.upd_membermaster BEFORE UPDATE
ON demo.membermaster
FOR EACH ROW
BEGIN
IF (new.memberdeposit &lt;&gt; 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
```
mysql&gt; update demo.membermaster set memberdeposit=20 where memberid = 2;
ERROR 1054 (42S22): Unknown column 'aa' in 'field list'
```
系统提示因为字段“aa”不存在导致触发器执行失败。现在我们来看看会员储值金额有没有被修改
```
mysql&gt; 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()函数来发现错误:
```
mysql&gt; select row_count();
+-------------+
| row_count() |
+-------------+
| -1 |
+-------------+
1 row in set (0.00 sec)
```
结果是-1说明我们可以通过ROW_COUNT()函数捕获到错误。
我们回顾一下对于记录会员储值金额变化的操作可以通过应用层发出SQL语句指令或者用一个存储过程来实现。无论哪种方式都要通过好几步相互关联的操作而且要做成一个事务处理过程复杂消耗的资源也较多。如果用触发器效率就会提高很多消耗的资源也少。同时还可以起到事务的类似功能保证关联操作全部完成或全部失败。
## 触发器的优缺点
通过刚刚的案例,你应该感受到触发器的高效了。现在,咱们把视角拔高一下,来看看触发器具体都有什么优缺点。毕竟,知己知彼,才能百战不殆。只有非常清楚它的优缺点,你才能充分发挥它的作用。
我先来说说触发器的优点。
**首先,触发器可以确保数据的完整性**。这是怎么体现的呢?我来举个小例子。
假设我们用进货单头表demo.importhead来保存进货单的总体信息包括进货单编号、供货商编号、仓库编号、总计进货数量、总计进货金额和验收日期。
<img src="https://static001.geekbang.org/resource/image/5b/e8/5b36cf11405b0159a1388ee23639aee8.jpeg" alt="">
用进货单明细表demo.importdetails来保存进货商品的明细包括进货单编号、商品编号、进货数量、进货价格和进货金额。
<img src="https://static001.geekbang.org/resource/image/b8/0e/b8e3fc8e001da9af2c85c22be5e6780e.jpeg" alt="">
每当我们录入、删除和修改一条进货单明细数据的时候,进货单明细表里的数据就会发生变动。这个时候,在进货单头表中的总计数量和总计金额就必须重新计算,否则,进货单头表中的总计数量和总计金额就不等于进货单明细表中数量合计和金额合计了,这就是数据不一致。
为了解决这个问题我们就可以使用触发器规定每当进货单明细表有数据插入、修改和删除的操作时自动触发2步操作
1. 重新计算进货单明细表中的数量合计和金额合计;
1. 用第一步中计算出来的值更新进货单头表中的合计数量与合计金额。
这样一来,进货单头表中的合计数量与合计金额的值,就始终与进货单明细表中计算出来的合计数量与合计金额的值相同,数据就是一致的,不会互相矛盾。
**其次,触发器可以帮助我们记录操作日志。**
利用触发器,可以具体记录什么时间发生了什么。我们前面的记录修改会员储值金额的触发器,就是一个很好的例子。这对我们还原操作执行时的具体场景,更好地定位问题原因很有帮助。
**另外,触发器还可以用在操作数据前,对数据进行合法性检查。**
举个小例子。超市进货的时候,需要库管录入进货价格。但是,人为操作很容易犯错误,比如说在录入数量的时候,把条形码扫进去了;录入金额的时候,看串了行,录入的价格远超售价,导致账面上的巨亏……这些都可以通过触发器,在实际插入或者更新操作之前,对相应的数据进行检查,及时提示错误,防止错误数据进入系统。
说了这么多触发器的优点,那是不是所有事件可以驱动的操作,都应该用触发器呢?要是你这么想,就掉坑里了。
下面我来说说触发器的缺点。
**触发器最大的一个问题就是可读性差。**
因为触发器存储在数据库中,并且由事件驱动,这就意味着触发器有可能不受应用层的控制。这对系统维护是非常有挑战的。
这是啥意思呢?我举个例子,你一看就明白了。
还是拿我们创建触发器时讲到的修改会员储值操作的那个触发器为例。如果触发器中的操作出了问题,会导致会员储值金额更新失败。我用下面的代码演示一下:
```
mysql&gt; update demo.membermaster set memberdeposit=20 where memberid = 2;
ERROR 1054 (42S22): Unknown column 'aa' in 'field list'
```
结果显示系统提示错误字段“aa”不存在。
这是因为触发器中的数据插入操作多了一个字段系统提示错误。可是如果你不了解这个触发器很可能会认为是更新语句本身的问题或者是会员信息表的结构出了问题。说不定你还会给会员信息表添加一个叫“aa”的字段试图解决这个问题结果只能是白费力。
另外,相关数据的变更,特别是数据表结构的变更,都可能会导致触发器出错,进而影响数据操作的正常运行。这些都会由于触发器本身的隐蔽性,影响到应用中错误原因排查的效率。
## 总结
今天这节课,我给你介绍了如何操作触发器。为了方便你学习,我汇总了相关的语法结构:
```
创建触发器的语法结构是
CREATE TRIGGER 触发器名称 {BEFORE|AFTER} {INSERT|UPDATE|DELETE}
ON 表名 FOR EACH ROW 表达式;
查看触发器的语句是:
SHOW TRIGGERS\G;
删除触发器的语法结构是:
DROP TRIGGER 触发器名称;
```
除此之外,我们还学习了触发器的优缺点。它的优点是可以确保数据一致性、记录操作日志和检查数据合法性。不过,它也存在可读性差,会增加系统维护的成本的缺点。在使用触发器的时候,你一定要综合考量。
最后,我还想再给你提一个小建议:**维护一个完整的数据库设计文档**。因为运维人员可能会经常变动,如果有一个完整的数据库设计文档,就可以帮助新人快速了解触发器的设计思路,从而减少错误,降低系统维护的成本。
## 思考题
我在课程中提到,每当在进货单明细表中插入或修改数据的时候,都要更新进货单头表中的总计数量和总计金额,这个问题可以用触发器来解决。你能不能创建一个触发器,要求是当操作人员更新进货单明细表中相关数据的时候,自动触发对进货单头表中相关数据的更改,确保数据的一致性?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。