MySQL/MariaDB觸發器


觸發器用來實現在永久表上進行某些操作時觸發啟動另一操作。

1.創建觸發器

以下是MariaDB中create trigger的語法:mysql不支持or replace和if not exists子句。

CREATE [OR REPLACE] TRIGGER [IF NOT EXISTS] trigger_name { BEFORE | AFTER } { INSERT | UPDATE | DELETE } ON tbl_name FOR EACH ROW trigger_body

觸發器只能建立在永久表上,不能建立在視圖和臨時表上。MySQL/MariaDB中的觸發器只支持行級觸發器(即每行都觸發一次觸發器),不支持數據庫級別和服務器級別的觸發器。MySQL/MariaDB中的觸發器雖然都是基於表的,卻存儲在數據庫下,理解這一點很重要,以后查看、刪除、引用trigger的時候都是通過數據庫名稱來引用的,而不是使用表來引用。

before和after是觸發時間,insert/update/delete是觸發事件例如before insert表示插入記錄之前觸發程序。其中before觸發器類似於SQL Server中的instead of觸發器,作用在檢查約束之前。而after觸發器和SQL Server中一樣,在檢查約束之后才生效。

下圖為SQL Server中instead of和after觸發器的工作位置。在MySQL/MariaDB中是一樣的,只要把MySQL/MariaDB中的概念和SQL Server中的概念對應起來即可。后文中有對該圖的分析。

在MySQL中,一張表只能有一個同時間、同事件的觸發器,所以MySQL中不支持基於列的觸發器。例如,一張表中可以存在before insert觸發器和before update,所以每張表最多只能有6個觸發器。但是MariaDB 10.2.3中可以為同時間、同事件創建多個觸發器。

在MySQL/MariaDB中,使用old和new表分別表示觸發器激活后的新舊表,在SQL Server中使用的是inserted和deleted表,其實它們的意義是等價的。但是坑爹的是MySQL/MariaDB中只能引用這兩張表中的列,而無法直接引用這兩張表。例如可以引用old.col_name,但是不能直接select * from old這樣引用old表。

old表表示刪除目標記錄之后將刪除的記錄保存在old表中,即deleted表。new表表示向表中插入新記錄之前,新記錄保存在new表中,即inserted表。或者說,只要涉及了insert相關的操作就有new表,只要涉及了delete相關的操作就有old表,而update操作基本可以認為是先delete再insert的行為,所以也會觸發這兩張表。

注意,即使是after觸發器,也是先將數據填充到old、new表中,再執行DML語句,最后激活觸發器執行觸發器中的語句。

在下面的小節中會分別驗證不同事件不同時間的觸發器行為。在驗證它們之前,先創建示例數據。

CREATE DATABASE IF NOT EXISTS test ;

USE test ;

CREATE OR REPLACE TABLE emp ( emp_no INT (11) NOT NULL, mgr_no INT (11) DEFAULT NULL, emp_name VARCHAR (30) DEFAULT NULL, PRIMARY KEY (emp_no) ) INSERT INTO emp (emp_no, mgr_no, emp_name) VALUES (1, NULL, 'David'), (2, 3, 'Mariah'), (3, 1, 'Tommy'), (4, 1, 'Jim'), (5, 3, 'Selina'), (6, 4, 'John'), (8, 3, 'Monty');

查看該表數據。

再創建一個極其簡單的審核表audit,該表前兩列為自增列和注釋列,后面的列結構等同於emp表。

DROP TABLE IF EXISTS audit;
CREATE TABLE audit AS SELECT * FROM emp WHERE 1=0;
ALTER TABLE audit ADD id INT AUTO_INCREMENT PRIMARY KEY FIRST;
ALTER TABLE audit ADD note CHAR(50) AFTER id;

2.insert觸發器

insert觸發器的作用是:當向表中插入數據的時候,將會激活觸發器。有兩類:before和after觸發器,分別表示數據插入到表中之前和數據插入到表中之后激活觸發器。

注意,只要向表中插入了新行,就會激活insert觸發器。插入新行的動作不僅僅只有insert語句,還有其他插入操作,例如load data語句、replace語句等等。

# 創建before insert觸發器 DELIMITER $$ CREATE OR REPLACE TRIGGER test.trig_demo1 BEFORE INSERT ON test.emp FOR EACH ROW BEGIN INSERT INTO audit VALUES(null,'before insert',new.emp_no,new.mgr_no,new.emp_name);
    END$$ DELIMITER ;

# 創建after insert觸發器 DELIMITER $$ CREATE OR REPLACE TRIGGER test.trig_demo2 AFTER INSERT ON test.emp FOR EACH ROW BEGIN INSERT INTO audit VALUES(null,'after insert',new.emp_no,new.mgr_no,new.emp_name); 
    END$$ DELIMITER ;

before insert觸發器的作用是:當向表emp中insert數據時,將首先激活該觸發器,該觸發器首先會將待插入數據填充到new表中,再向審核表audit中插入一行數據,並標明此次觸發操作是"before insert"。觸發器執行結束后,才開始向emp表中插入數據。

after insert觸發器的作用是:當向表emp中insert數據時,將先將數據填充到new表中,再插入到emp表,之后激活該觸發器,該觸發器會向審核表audit中插入一行數據,並標明此次觸發操作是"after insert"。

現在向emp表中插入數據進行測試。

INSERT INTO emp VALUES(10,3,'longshuai');

插入之后,查看audit表。

MariaDB [test]> select * from audit;
+----+---------------+--------+--------+-----------+
| id | note          | emp_no | mgr_no | emp_name  |
+----+---------------+--------+--------+-----------+
|  1 | before insert |     10 |      3 | longshuai |
|  2 | after insert  |     10 |      3 | longshuai |
+----+---------------+--------+--------+-----------+

可以看到,一次insert操作觸發了before insert和after insert兩個觸發器。且無論是before還是after insert觸發器都有new表的存在。

在mariadb 10.2.3版本之后,一個表中可以為同一時間、同一事件創建多個觸發器(在mysql中不允許)。例如:

# 創建第二個after insert觸發器 DELIMITER $$ CREATE OR REPLACE TRIGGER test.trig_demo3 AFTER INSERT ON test.emp FOR EACH ROW BEGIN INSERT INTO audit VALUES(null,'after insert2',new.emp_no,new.mgr_no,new.emp_name); 
    END$$ DELIMITER ;
show triggers;

此處刪除新建的這個trigger,注意刪除trigger的時候是通過數據庫名稱來也引用trigger的,而不是table名稱。

drop trigger test.trig_demo3;

3.delete觸發器

delete觸發器的作用是:當刪除表中數據記錄的時候,將會激活觸發器。

有兩類insert觸發器:before和after觸發器,分別表示表中記錄被刪除之前和表中數據被刪除之后激活觸發器。

注意,delete觸發器只在表中記錄被刪除的時候才會被激活。例如delete語句、replace語句。但是drop語句、truncate語句不會激活delete觸發器,因為它們是DDL語句,而MySQL/MariaDB不支持DDL觸發器,它們並沒有對表中的記錄執行delete操作。

# 創建before delete觸發器 DELIMITER $$ CREATE OR REPLACE TRIGGER test.trig_demo3 BEFORE DELETE ON test.emp FOR EACH ROW BEGIN INSERT INTO audit VALUES(NULL,'before delete',old.emp_no,old.mgr_no,old.emp_name); 
    END$$ DELIMITER ;

# 創建after delete觸發器 DELIMITER $$ CREATE OR REPLACE TRIGGER test.trig_demo4 AFTER DELETE ON test.emp FOR EACH ROW BEGIN INSERT INTO audit VALUES(NULL,'after delete',old.emp_no,old.mgr_no,old.emp_name); 
    END$$ DELIMITER ;

這兩個delete事件的觸發器作用很簡單,先將待刪除的記錄插入到old表中,再在刪除表中的記錄之前、之后,向審核表audit中插入一行'before delete'或'after delete'的審核日志。

現在刪除emp表中的一行記錄進行測試。

delete from emp where emp_no=10;

刪除emp表中數據之后,查看audit表。

MariaDB [test]> SELECT * FROM audit;
+----+---------------+--------+--------+-----------+
| id | note          | emp_no | mgr_no | emp_name  |
+----+---------------+--------+--------+-----------+
|  1 | before insert |     10 |      3 | longshuai |
|  2 | after insert  |     10 |      3 | longshuai |
|  3 | before delete |      0 |   NULL | NULL      |
|  4 | after delete  |      0 |   NULL | NULL      |
+----+---------------+--------+--------+-----------+

可見,一次delete操作觸發了before delete和after delete觸發器。且刪除記錄前后old表都存在。

4.update觸發器

update觸發器的作用是:當表中數據記錄被修改的時候,將會激活觸發器。

有兩類update觸發器:before和after觸發器,分別表示表中記錄被修改之前和表中數據被修改之后激活觸發器。

注意,update操作可以認為是先delete再insert,因此它將填充old表和new表。

# 創建before update觸發器 DELIMITER $$ CREATE OR REPLACE TRIGGER test.trig_demo5 BEFORE UPDATE ON test.emp FOR EACH ROW BEGIN INSERT INTO audit VALUES(NULL,'before update from new',new.emp_no,new.mgr_no,new.emp_name);
        INSERT INTO audit VALUES(NULL,'before update from old',old.emp_no,old.mgr_no,old.emp_name);
    END$$ DELIMITER ; 

# 創建after update觸發器 DELIMITER $$ CREATE OR REPLACE TRIGGER test.trig_demo6 AFTER UPDATE ON test.emp FOR EACH ROW BEGIN INSERT INTO audit VALUES(NULL,'after update from new',new.emp_no,new.mgr_no,new.emp_name);
        INSERT INTO audit VALUES(NULL,'after update from old',old.emp_no,old.mgr_no,old.emp_name); 
    END$$ DELIMITER ;

before update觸發器的作用是:當更新emp表中的一條記錄時,首先將表中該行記錄插入到old表中,待更新結果插入到new表中,然后激活觸發器,向審核表中寫入數據,最后修改emp表中的記錄。
after update觸發器的作用是:當更新emp表中的一條記錄時,首先將表中該行記錄插入到old表中,待更新結果插入到new表中,然后修改emp表中的記錄,最后激活觸發器,向審核表中寫入數據。

更新emp表中一行記錄。

update emp set emp_no=7 where emp_no=8;

查看audit表。

MariaDB [test]> select * from audit;
+----+------------------------+--------+--------+-----------+
| id | note                   | emp_no | mgr_no | emp_name  |
+----+------------------------+--------+--------+-----------+
|  1 | before insert          |     10 |      3 | longshuai |
|  2 | after insert           |     10 |      3 | longshuai |
|  3 | before delete          |      0 |   NULL | NULL      |
|  4 | after delete           |      0 |   NULL | NULL      |
|  5 | before update from new |      7 |      3 | Monty     |
|  6 | before update from old |      8 |      3 | Monty     |
|  7 | after update from new  |      7 |      3 | Monty     |
|  8 | after update from old  |      8 |      3 | Monty     |
+----+------------------------+--------+--------+-----------+

可以看到,一次update操作觸發了before update觸發器和after update觸發器,並且update操作時,new和old兩張表中都有新舊數據。上面的結果中from new對應的是更新后的數據,來源於更新前填充的new表,from old對應的是更新前的舊數據,來源於更新前填充的old表。

5.通過on duplicate key update分析觸發器觸發原理

在MySQL/MariaDB中,如果向表中插入的數據有重復沖突檢測時會阻止插入。解決這個問題的其中一個方法就是使用on duplicate key update子句。這個子句應用在insert字句中,但其中涉及到了update操作,那到底會觸發哪些觸發器呢?

這里先清空上面的audit表。

TRUNCATE audit;

首先測試下使用on duplicate key update子句插入無重復的記錄。注意,emp表的emp_no列具有主鍵屬性,它不允許出現重復值。

INSERT INTO emp VALUES(15,5,'xiaofang') ON DUPLICATE KEY UPDATE emp_name='xiaofang';

查看audit表。

MariaDB [test]> select * from audit;
+----+---------------+--------+--------+----------+
| id | note          | emp_no | mgr_no | emp_name |
+----+---------------+--------+--------+----------+
|  1 | before insert |     15 |      5 | xiaofang |
|  2 | after insert  |     15 |      5 | xiaofang |
+----+---------------+--------+--------+----------+

可以看到,在插入沒有重復沖突的行只觸發了before insert和after insert觸發器。沒有觸發update觸發器。

再插入一條有重復沖突的記錄。

TRUNCATE audit;
INSERT INTO emp VALUES(3,1,'xiaofang') ON DUPLICATE KEY UPDATE emp_name='xiaofang';

查看audit表:

MariaDB [test]> select * from audit;
+----+------------------------+--------+--------+----------+
| id | note                   | emp_no | mgr_no | emp_name |
+----+------------------------+--------+--------+----------+
|  1 | before insert          |      3 |      1 | xiaofang |
|  2 | before update from new |      3 |      1 | xiaofang |
|  3 | before update from old |      3 |      1 | Tommy    |
|  4 | after update from new  |      3 |      1 | xiaofang |
|  5 | after update from old  |      3 |      1 | Tommy    |
+----+------------------------+--------+--------+----------+

可以看到,這里觸發了3個觸發器:before insert/before update/after update,為什么前面只觸發了兩個insert觸發器而這里觸發了3個觸發器。其實根據下面的圖很好分析。

insert into... on duplicate key update語句中,插入沒有重復值沖突的記錄時,首先判斷是否存在before insert觸發器,有就觸發,觸發之后檢查約束,發現沒有重復值沖突,然后直接觸發after insert觸發器。所以這種情況下只觸發了before insert和after insert觸發器。

而插入有重復值沖突的記錄時,首先觸發了before insert觸發器,然后檢查約束發現存在重復值沖突,所以改insert操作為update操作,update操作再次回到事務的頂端,先觸發before update再檢查約束,這時候已經不再重復值沖突,所以后面觸發after update觸發器。

6.replace into算法驗證

插入新記錄時,對於重復值沖突的記錄,使用replace into語句代替insert into是另一種方法。這種方法實現方式和on duplicate key update方式不一樣。

replace into算法說明如下:

  1. 嘗試插入新行。
  2. 存在重復值沖突時,從表中刪除重復行。
  3. 將新行插入到表中。

也就是說,存在重復值沖突時,如果使用觸發器的話,將先觸發before insert,再觸發delete操作,先是before delete再是after delete,最后觸發after insert。

以下是驗證過程和結果:首先清空audit表,再插入重復沖突的記錄。

TRUNCATE audit;
REPLACE INTO emp VALUES(3,1,'gaoxiaofang');

查看audit表:

MariaDB [test]> select * from audit;
+----+---------------+--------+--------+-------------+
| id | note          | emp_no | mgr_no | emp_name    |
+----+---------------+--------+--------+-------------+
|  1 | before insert |      3 |      1 | gaoxiaofang |
|  2 | before delete |      0 |   NULL | NULL        |
|  3 | after delete  |      0 |   NULL | NULL        |
|  4 | after insert  |      3 |      1 | gaoxiaofang |
+----+---------------+--------+--------+-------------+

顯然,和算法說明的結果是對應的。

7.查看、刪除觸發器

mysql> SHOW CREATE TRIGGER trig_demo5\G
*************************** 1. row ***************************
 Trigger: trig_demo5
 sql_mode: 
SQL Original Statement: CREATE DEFINER=`root`@`192.168.100.%` TRIGGER `test`.`trig_demo5` BEFORE UPDATE ON `test`.`emp`
 FOR EACH ROW BEGIN
 INSERT INTO audit VALUES(NULL,'before update from new',new.emp_no,new.mgr_no,new.emp_name);
 INSERT INTO audit VALUES(NULL,'before update from old',old.emp_no,old.mgr_no,old.emp_name);
 END
  character_set_client: utf8
  collation_connection: utf8_general_ci
 Database Collation: latin1_swedish_ci
mysql> show triggers;
mysql> show trigger like 'pattern';
mysql> show trigger where 'expression';

但是要注意,這個like的模式是對表名進行匹配的,而不是觸發器名。例如觸發器trig_demo1是基於emp表創建的,則使用like 'emp'而不能使用like 'trig_demo1'。

在information_schema中有TRIGGERS元數據表:

例如:

mysql> select * from information_schema.triggers where trigger_name='trig_demo1'\G
*************************** 1. row ***************************
           TRIGGER_CATALOG: def
            TRIGGER_SCHEMA: test
              TRIGGER_NAME: trig_demo1
        EVENT_MANIPULATION: INSERT
      EVENT_OBJECT_CATALOG: def
       EVENT_OBJECT_SCHEMA: test
        EVENT_OBJECT_TABLE: emp
              ACTION_ORDER: 0
          ACTION_CONDITION: NULL
          ACTION_STATEMENT: BEGIN
        INSERT INTO audit VALUES(null,'before insert',NEW.emp_no,new.mgr_no,new.emp_name);
    END
        ACTION_ORIENTATION: ROW
             ACTION_TIMING: BEFORE
ACTION_REFERENCE_OLD_TABLE: NULL
ACTION_REFERENCE_NEW_TABLE: NULL
  ACTION_REFERENCE_OLD_ROW: OLD
  ACTION_REFERENCE_NEW_ROW: NEW
                   CREATED: NULL
                  SQL_MODE: 
                   DEFINER: root@192.168.100.%
      CHARACTER_SET_CLIENT: utf8
      COLLATION_CONNECTION: utf8_general_ci
        DATABASE_COLLATION: latin1_swedish_ci
1 row in set (0.00 sec)

刪除觸發器的時候,需要使用drop語句指定數據庫名,而不是指定表名稱。例如:

DROP TRIGGER [ IF EXISTS ] test.example_trigger;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM