4. SQL編程之MySQL 觸發(fā)器

SQL編程之MySQL 觸發(fā)器先介紹一下觸發(fā)器trigger的基本概念:

  • 一個觸發(fā)器一定是綁定在某個table上面的,不能像function和procedure那樣可以單獨存在;
  • 它不需要人為的去調(diào)用,當達到特定條件后,由MySQL自動調(diào)用或者說自動觸發(fā),所以叫做觸發(fā)器;
  • 觸發(fā)的事件分為 INSERT,UPDATE,DELETE三種;
  • 解發(fā)的時機分為 BEFORE,AFTER 代表事件發(fā)生之前觸發(fā)和事件發(fā)生之后觸發(fā);

由此可見我們可以定義6種觸發(fā)器,分別是INSERT的前后,UPDATE的前后及DELETE的前后;

觸發(fā)器名字的命名規(guī)范

全名: t2_b4_insert_do_not_update_pk

t1: 表名
b4: 觸發(fā)時機,對應(yīng) before
insert:觸發(fā)事件
do_not_update_pk: 功能描述

編寫一個不允許修改主鍵的trigger

接下來,我們,BEFORE UPDATE為例,揭開trigger的神密面紗.
BEFORE UPDATE:從字面就很好理解是在執(zhí)行UPDATE之前被自動觸發(fā),執(zhí)行過程中出現(xiàn)錯誤,就會中斷本次UPDATE,并可以拋出自定義的異常.在trigger中,有兩個最重要的關(guān)鍵字NEW和OLD, 分別代表修改前的數(shù)據(jù)對象和修改后的數(shù)據(jù)對象,如果我們需要判斷某個字段的值是否發(fā)生了更新可以這樣做:

-- 如果是NOT NULL的字段
IF NEW.字段名 <> OLD.字段名 THEN
  --do something;
END IF;

-- 非NOT NULL的字段的判斷方法, 大家可以仔細體會一下這種寫法
IF IFNULL(NEW.字段名,1) <> IFNULL(OLD.字段名,1) THEN
  --do something;
END IF;

來一下完整的例子吧:

DELIMITER $$

DROP TRIGGER IF EXISTS T2_B4_UPDATE_DO_NOT_UPDATE_PK $$
CREATE TRIGGER T2_B4_UPDATE_DO_NOT_UPDATE_PK BEFORE UPDATE ON t2
FOR EACH ROW
BEGIN
  -- 功能: 不允許修改主建

  -- 錯誤提示
  DECLARE C_ERROR_MSG VARCHAR(100);
  
  -- 判斷主鍵是否被修改了
  IF NEW.ID <> OLD.ID THEN
    -- 錯誤消息
    SET C_ERROR_MSG = 'B4TriggerError: Primary key does not allow modification';
    -- 拋出異常, 事務(wù)會被rollback
    SIGNAL SQLSTATE '09001' SET MESSAGE_TEXT = C_ERROR_MSG, MYSQL_ERRNO = 9001;
  END IF;
END$$

DELIMITER ;

測試效果:

# 修改之前的數(shù)據(jù)
mysql> select * from t2;
+----+------+------------+------+------------+
| id | nid  | cid        | n1   | c1         |
+----+------+------------+------+------------+
...
| 10 |  939 | UUUUUUUUUU |  939 | UUUUUUUUUU |
| 11 |  196 | FFFFFFFFFF |  196 | FFFFFFFFFF |
+----+------+------------+------+------------+
10 rows in set (0.00 sec)

# 修改id失敗
mysql> update t2 set id=12 where id=11;
ERROR 9001 (09001): B4TriggerError: Primary key does not allow modification

# 確數(shù)據(jù)沒有被修改
mysql> select * from t2;
+----+------+------------+------+------------+
| id | nid  | cid        | n1   | c1         |
+----+------+------------+------+------------+
...
| 10 |  939 | UUUUUUUUUU |  939 | UUUUUUUUUU |
| 11 |  196 | FFFFFFFFFF |  196 | FFFFFFFFFF |
+----+------+------------+------+------------+
10 rows in set (0.00 sec)

# 修改其他字段成功
mysql> update t2 set nid=999 where id=11;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from t2;
+----+------+------------+------+------------+
| id | nid  | cid        | n1   | c1         |
+----+------+------------+------+------------+
...
| 10 |  939 | UUUUUUUUUU |  939 | UUUUUUUUUU |
| 11 |  999 | FFFFFFFFFF |  196 | FFFFFFFFFF |
+----+------+------------+------+------------+
10 rows in set (0.00 sec)

一個有趣的測試:

mysql> select * from t2;
+----+------+------------+------+------------+
| id | nid  | cid        | n1   | c1         |
+----+------+------------+------+------------+
...
| 10 |  999 | UUUUUUUUUU |  939 | UUUUUUUUUU |
| 11 |  999 | FFFFFFFFFF |  196 | FFFFFFFFFF |
+----+------+------------+------+------------+
10 rows in set (0.00 sec)

# 開啟一個事務(wù)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t2 set nid=888 where id=10;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> update t2 set nid=888 where id=11;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

# 這里發(fā)生了一個錯誤
mysql> update t2 set id=12 where id=11;
ERROR 9001 (09001): B4TriggerError: Primary key does not allow modification

# 提交這個事務(wù)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

# 此時,葉老師該登場了, 請問大家來猜猜nid=888呢,還是999? 經(jīng)過同學們的認真思考投票后, 仍發(fā)現(xiàn), 真理還是在少數(shù)人的手中, 哈哈...
mysql> select * from t2;
+----+------+------------+------+------------+
| id | nid  | cid        | n1   | c1         |
+----+------+------------+------+------------+
...
| 10 |  888 | UUUUUUUUUU |  939 | UUUUUUUUUU |
| 11 |  888 | FFFFFFFFFF |  196 | FFFFFFFFFF |
+----+------+------------+------+------------+
10 rows in set (0.00 sec)

納尼!? 納尼!? 納尼!?
Innodb不是支持事務(wù)的原子性嗎? 為什么會出現(xiàn)事務(wù)的部分提交,部分回滾. Monty,你個大騙子! Heikki,你個大騙子! Oracle,你也是大騙子! 葉老師,.....空氣突然變得安靜.....,你更是個大騙子!

喝了一杯涼水,稍微冷靜了一下: 哦,不對,是trigger破壞了事務(wù)的原子性,trigger根本就不能用,偉哥才是最大的騙子,凈整些沒有用東西來浪費我們的時間.

一氣之下喝下了一壺涼水,終于冷靜下了, 如果trigger真有這么嚴重的問題, 為何還要設(shè)計它, 顯得逼格高嗎? 還是得從頭來分析分析, 為什么會這樣, 再回顧一下:

# 開啟一個事務(wù)
mysql> begin;
# 正常的更新
mysql> update t2 set nid=888 where id=10;
mysql> update t2 set nid=888 where id=11;

# 發(fā)生了錯誤的更新,這里出錯了...,這里出錯了...,這里出錯了...
mysql> update t2 set id=12 where id=11;
ERROR 9001 (09001): B4TriggerError: Primary key does not allow modification

# 提交這個事務(wù)
mysql> commit;

咦?! 啊?! 哦!?
一陣感嘆過后,終于看出了問題. 偉哥, 你太壞了! 故意挖坑讓我跳, 害得我把全世界的人都得罪了! 事務(wù)執(zhí)行過程已經(jīng)產(chǎn)生了錯誤, 說明事務(wù)就不能再提交, 你還故意提交? 還要讓我們猜? 你是世上最壞的人, 對對對, 沒有之一!

  • Monty, 你真是個好人, 這么好的產(chǎn)品,還免費給我們用
  • Heikki, 你真是個牛人, 搞個InnoDB,直接把MySQL帶飛
  • Oracle大咖,你是全球最棒的數(shù)據(jù)庫專家,對對對,也沒有之一
  • 葉老師,今后誰也動搖不了你在我心中男神的地位,一日為師終身為父, 更何況您是教我如何賺錢生存的老師, 都怪我學得太膚淺, 這么容易就上了偉哥的當
  • 偉哥,你你你,你真的有當過兵嗎?軍人不都憨厚老實嗎? 你怎么這么太調(diào)皮...

說歸說, 鬧歸鬧, 最后我們再認真小結(jié)一下:

  • 來點段子能讓你更容易記住重點;
  • 一般情況是不會出現(xiàn)的上面這種情況的,因為事務(wù)原子性的設(shè)計本身就是需要每個動作都成功后再提交;
  • 二般情況,就是像偉哥這樣,明明有錯誤發(fā)生了,執(zhí)意要commit,可以拿來坑人,可以拿來面試;
  • 如果你全明白了的話,就可以出臺掙錢了;
  • 如果你還是似懂非懂的話,請繼續(xù)往下看;

MySQL雙班的同學可能有一部分同學是沒有編程經(jīng)驗的,偉哥常說一句口頭禪,"好人做到底,送佛送到西",再給大家寫個偽代碼,幫助大家進一步理解在編程語言中,是如何處理事務(wù)提交和回滾的:

# 不管是什么語言,異常(或叫錯誤)捕獲語法,看起來像下面這樣
try{
  # MySQL層面的邏輯
  mysql.query(update t2 set nid=888 where id=10)
  mysql.query(update t2 set nid=888 where id=11)
  mysql.query(update t2 set id=12 where id=11)
  
  # 非MySQL的邏輯處理, 如網(wǎng)絡(luò),文件等等
  other.do_other_things()
  
  # 開始提交,如果上面的任何地方出現(xiàn)在了異常,都會轉(zhuǎn)到 error{}中執(zhí)行,異常代碼下面的語句直接被跳過
  mysql.query(commit)
}error{
  # 任何代碼發(fā)生錯誤后都會轉(zhuǎn)到這里開始執(zhí)行,最常見的就是處理事務(wù)的回滾
  mysql.query(rollback)
}

# 偉哥你又騙人,我在bash手冊中找了一天,也沒發(fā)現(xiàn)這個語法
# 同學, bash那是腳本,不是編程語言,你沒有認真聽吳老師講課嗎?

# 又有同學說,偉哥,你這段程序是什么語言呀,我用Python,PHP,Java,GO,C++都試過了,怎么全部報錯呀.偽代碼屬于什么語言呀?
# 同學,偽代碼是一種虛擬代碼,通常用來簡化描述一種算法或程序的關(guān)鍵實現(xiàn)方法或過程,你全明白了嗎?

現(xiàn)在應(yīng)該全明白了吧? 不過偉哥勸你還先別出臺掙錢, 咱們后面還有很多干貨. 偉哥從來不騙人的. 這次是認真的.

編寫一個自動記錄行更新版本的trigger

再來一個例子:

# 先增加一個字段
mysql> alter table t2 add version int not null default 0 comment '版本號';
Query OK, 0 rows affected (0.65 sec)

mysql> select * from t2;
+----+------+------------+------+------------+---------+
| id | nid  | cid        | n1   | c1         | version |
+----+------+------------+------+------------+---------+
|  1 |  522 | RRRRRRRRRR |  522 | RRRRRRRRRR |       0 |
|  2 |   69 | TTTTTTTTTT |   69 | TTTTTTTTTT |       0 |
|  3 |  609 | ssssssssss |  609 | ssssssssss |       0 |
|  4 |  949 | oooooooooo |  949 | oooooooooo |       0 |
|  5 |  559 | IIIIIIIIII |  559 | IIIIIIIIII |       0 |
|  6 |   71 | pppppppppp |   71 | pppppppppp |       0 |
|  7 |  325 | pppppppppp |  325 | pppppppppp |       0 |
|  8 |  533 | ssssssssss |  533 | ssssssssss |       0 |
| 10 |  888 | UUUUUUUUUU |  939 | UUUUUUUUUU |       0 |
| 11 |  888 | FFFFFFFFFF |  196 | FFFFFFFFFF |       0 |
+----+------+------------+------+------------+---------+
10 rows in set (0.00 sec)

再建一個trigger:

DELIMITER $$

DROP TRIGGER IF EXISTS T2_B4_UPDATE_AUTO_UPDATE_VERSION $$
CREATE TRIGGER T2_B4_UPDATE_AUTO_UPDATE_VERSION BEFORE UPDATE ON t2
FOR EACH ROW
BEGIN
  -- 功能: 自動更新版本號
  
  -- 累加版本號
  SET NEW.VERSION = OLD.VERSION + 1;
END$$

DELIMITER ;

看看效果:

# 創(chuàng)建新的觸發(fā)器,請注意MySQL 5.6+的版本才為一個表創(chuàng)建多個同類型的觸發(fā)器
# 這樣的好處是,可以把不同功能或業(yè)務(wù)的邏輯定義到不同的trigger中,并以名稱進行區(qū)分,便于維護
mysql> DELIMITER $$
mysql>
mysql> DROP TRIGGER IF EXISTS T2_B4_UPDATE_AUTO_UPDATE_VERSION $$
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> CREATE TRIGGER T2_B4_UPDATE_AUTO_UPDATE_VERSION BEFORE UPDATE ON t2
    -> FOR EACH ROW
    -> BEGIN
    ->   -- 功能: 自動更新版本號
    ->   
    ->   -- 累加版本號
    ->   SET NEW.VERSION = OLD.VERSION + 1;
    -> END$$
Query OK, 0 rows affected (0.01 sec)

mysql>
mysql> DELIMITER ;

測試一下:

# 請意觀察 version
mysql> select * from t2 where id=1;
+----+------+------------+------+------------+---------+
| id | nid  | cid        | n1   | c1         | version |
+----+------+------------+------+------------+---------+
|  1 |  522 | RRRRRRRRRR |  522 | RRRRRRRRRR |       0 |
+----+------+------------+------+------------+---------+
1 row in set (0.00 sec)

mysql> update t2 set c1='alvin zane' where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from t2 where id=1;
+----+------+------------+------+------------+---------+
| id | nid  | cid        | n1   | c1         | version |
+----+------+------------+------+------------+---------+
|  1 |  522 | RRRRRRRRRR |  522 | alvin zane |       1 |
+----+------+------------+------+------------+---------+
1 row in set (0.00 sec)

mysql> update t2 set c1='I' where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from t2 where id=1;
+----+------+------------+------+------+---------+
| id | nid  | cid        | n1   | c1   | version |
+----+------+------------+------+------+---------+
|  1 |  522 | RRRRRRRRRR |  522 | I    |       2 |
+----+------+------------+------+------+---------+
1 row in set (0.00 sec)

mysql> update t2 set c1='like' where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from t2 where id=1;
+----+------+------------+------+------+---------+
| id | nid  | cid        | n1   | c1   | version |
+----+------+------------+------+------+---------+
|  1 |  522 | RRRRRRRRRR |  522 | like |       3 |
+----+------+------------+------+------+---------+
1 row in set (0.00 sec)

mysql> update t2 set c1='trigger' where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from t2 where id=1;
+----+------+------------+------+---------+---------+
| id | nid  | cid        | n1   | c1      | version |
+----+------+------------+------+---------+---------+
|  1 |  522 | RRRRRRRRRR |  522 | trigger |       4 |
+----+------+------------+------+---------+---------+
1 row in set (0.00 sec)

編寫一個自動記錄行更新歷史記錄的trigger

還沒有完,再來一個例子:

-- 請留意t2_history的PRIMARY KEY

CREATE TABLE `t2_history` (
  `id` int(11) NOT NULL,  
  `version` int(11) NOT NULL DEFAULT '0' COMMENT '版本號',
  `nid` int(11) DEFAULT NULL,
  `cid` varchar(10) DEFAULT NULL,
  `n1` int(11) DEFAULT NULL,
  `c1` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`,`version`)
) ENGINE=InnoDB;

DELIMITER $$

DROP TRIGGER IF EXISTS T2_B4_UPDATE_AUTO_BACKUP $$
CREATE TRIGGER T2_B4_UPDATE_AUTO_BACKUP BEFORE UPDATE ON t2
FOR EACH ROW
BEGIN
  -- 功能: 自動備份歷史副本
  
  -- INSERT到歷史表,(ID,VERSION)總是記錄,其它列只記錄有變化的數(shù)據(jù),默認為NULL
  INSERT INTO t2_history SET
    ID=OLD.ID,
    VERSION=OLD.VERSION,
    NID=IF(IFNULL(OLD.NID,1)<>IFNULL(NEW.NID,1),OLD.NID,NULL),
    CID=IF(IFNULL(OLD.CID,1)<>IFNULL(NEW.CID,1),OLD.CID,NULL),
    N1=IF(IFNULL(OLD.N1,1)<>IFNULL(NEW.N1,1),OLD.N1,NULL),
    C1=IF(IFNULL(OLD.C1,1)<>IFNULL(NEW.C1,1),OLD.C1,NULL);

END$$

DELIMITER ;

這里就省去創(chuàng)建table和trigger的過程了,直接開始測試:

mysql> update t2 set c1='I' where id=3;
mysql> update t2 set c1='love' where id=3;
mysql> update t2 set c1='python' where id=3;
mysql> update t2 set c1='too' where id=3;
Query OK, 1 row affected (0.00 sec)

mysql> select * from t2_history where id=3;
+----+---------+------+------+------+------------+
| id | version | nid  | cid  | n1   | c1         |
+----+---------+------+------+------+------------+
|  3 |       0 | NULL | NULL | NULL | ssssssssss |
|  3 |       1 | NULL | NULL | NULL | I          |
|  3 |       2 | NULL | NULL | NULL | love       |
|  3 |       3 | NULL | NULL | NULL | python     |
+----+---------+------+------+------+------------+
4 rows in set (0.00 sec)

# 查詢c1字段的所有版本,只需要這樣:
select id,version,c1 from t2_history where id=3
union all
select id,version,c1 from t2 where id=3;
+----+---------+------------+
| id | version | c1         |
+----+---------+------------+
|  3 |       0 | ssssssssss |
|  3 |       1 | I          |
|  3 |       2 | love       |
|  3 |       3 | python     |
|  3 |       4 | too        |
+----+---------+------------+
5 rows in set (0.00 sec)

再把修改人,修改時間也記錄起來, 看起來一切都接近完美了. 再沒有什么東西可以逃出DBA的法眼了,你懂的.

有了觸發(fā)器后,正常的一個update除了指定表外,可能還會產(chǎn)生其它的數(shù)據(jù)更新,像上面的t2_history的新增. 這個情況下,binlog(row格式)會怎么記錄呢? 從庫的trigger會不會重復產(chǎn)生binlog呢?

好了,又到菠菜的時候了,請下注:

  • a. binlog只記錄t2表的更新,t2_history表的由從庫的trigger自動生成;
  • b. binlog同時記錄t2,t2_history表的更新,從庫trigger不會被觸發(fā);
  • c.binlog同時記錄t2,t2_history表的更新,從庫trigger會被觸發(fā);

請允許我再調(diào)皮一下,請自己動手驗證你的答案吧.

小結(jié)trigger的相關(guān)概念一下:

  • trigger綁定在table下
  • 它隨時在監(jiān)控table執(zhí)行INSERT,UPDATE,DELETE的情況,
  • 它可以獲取到執(zhí)行DML時的兩種數(shù)據(jù)內(nèi)容,分別是NEW.字段名和OLD.字段名
  • 它可以改變DML的結(jié)果
  • 它可以終止DML的執(zhí)行,并拋出異常通知上游語言回滾事務(wù)
  • 它可以產(chǎn)生新數(shù)據(jù),如執(zhí)行其它表的DML語句
  • 它還可以function和procedure一起使用
  • 更多東西,等你去挖掘

最后總結(jié)一下本節(jié)主要內(nèi)容:

  • 觸發(fā)器的基本概念,觸發(fā)事件,觸發(fā)時間
  • 觸發(fā)器名字的命名規(guī)范
  • 編寫一個不允許修改主鍵的trigger
  • 編寫一個自動記錄行更新版本的trigger
  • 編寫一個自動記錄行更新歷史記錄的trigger

返回目錄

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容