MySQL進階三板斧(三)看清“觸發器 (Trigger)”的真實面目

觸發器(Trigger)的起源

  • MySQL是最受歡迎的開源RDBMS,被社區和企業廣泛使用。觸發器是MySQL在5.0.1(開天闢地一版本)中增加的三大新功能之一,另外兩個師兄弟是視圖(view)與存儲過程(procedure)。均屬於相對“高級”一點的數據庫必需功能。

目錄

 

一、什麼是觸發器

MySQL觸發器的作用

二、觸發器語法

1. 創建語法四要素

2. 語法公式

3. 語法參數說明

4. NEW 與 OLD關鍵字詳解

三、實際應用

1. 數據準備

2. 使用案例

# Insert觸發器 - 級聯插入

# Delete觸發器 - 級聯刪除

# Update觸發器 - 實時更新

四、觸發器有哪些缺陷 @江湖中人

五、總結


一、什麼是觸發器


       觸發器(trigger):監視某種情況,並觸發執行某種操作。觸發器是在表中數據發生更改時自動觸發執行的,它是與表事件相關的特殊的存儲過程,它的執行不是由程序調用,也不是手工啓動,而是由事件來觸發,例如當對一個表進行操作(insert,delete, update)時就會激活它執行。也就是說觸發器只執行DML事件(insert、update和delete)


MySQL觸發器的作用

1. 安全性。可以基於數據庫的值使用戶具有操作數據庫的某種權利。

  • 可以基於時間限制用戶的操作,例如不允許下班後和節假日修改數據庫數據。
  • 可以基於數據庫中的數據限制用戶的操作,例如不允許股票的價格的升幅一次超過10%。

2. 審計。可以跟蹤用戶對數據庫的操作。   

  • 審計用戶操作數據庫的語句。
  • 把用戶對數據庫的更新寫入審計表。

3. 實現複雜的數據完整性規則

  • 實現非標準的數據完整性檢查和約束。觸發器可產生比規則更爲複雜的限制。與規則不同,觸發器可以引用列或數據庫對象。例如,觸發器可回退任何企圖吃進超過自己保證金的期貨。
  • 提供可變的缺省值。

4. 實現複雜的非標準的數據庫相關完整性規則。觸發器可以對數據庫中相關的表進行連環更新

例如,在auths表author_code列上的刪除觸發器可導致相應刪除在其它表中的與之匹配的行。

  • 在修改或刪除時級聯修改或刪除其它表中的與之匹配的行。
  • 在修改或刪除時把其它表中的與之匹配的行設成NULL值。
  • 在修改或刪除時把其它表中的與之匹配的行級聯設成缺省值。
  • 觸發器能夠拒絕或回退那些破壞相關完整性的變化,取消試圖進行數據更新的事務。當插入一個與其主健不匹配的外部鍵時,這種觸發器會起作用。例如,可以在books.author_code 列上生成一個插入觸發器,如果新值與auths.author_code列中的某值不匹配時,插入被回退。

5. 同步實時地複製表中的數據。

6. 自動計算數據值,如果數據的值達到了一定的要求,則進行特定的處理。例如,如果公司的帳號上的資金低於5萬元則立即給財務人員發送警告數據。


二、觸發器語法

1. 創建語法四要素

 

  1. 監視地點(table)
  2. 監視事件(insert | update | delete)
  3. 觸發時間(after | before)
  4. 觸發事件(insert | update | delete)

2. 語法公式

CREATE TRIGGER <trigger_name>  
  
  BEFORE|AFTER

  INSERT|UPDATE|DELETE  ON <table_name> # 表名
  
  FOR EACH ROW  # 這句話在mysql是固定的

  BEGIN

  <觸發的SQL語句>(調用NEW/OLD參數);

  END

3. 語法參數說明

  1. CREATE TRIGGER <trigger_name>  --- 觸發器必須有名字,最多64個字符,可能後面會附有分隔符.它和MySQL中其他對象的命名方式基本相象.
  2. { BEFORE | AFTER }  --- 觸發器觸發時間設置:可以設置爲事件發生前或後(前:一般用於校驗;後:一般用於關聯)。
  3. { INSERT | UPDATE | DELETE }  -- 設定觸發事件:如執行insert、update或delete的過程時激活觸發器。
  4. ON <table_name>  --- 觸發器是屬於某一個表的: 當在這個表上執行 INSERT|UPDATE|DELETE 操作的時候就導致觸發器的激活. 同時,我們不能給同一張表的“同一個事件”安排兩個觸發器(意味着不能同時有兩個Insert觸發器)。
  5. FOR EACH ROW  --- 觸發器的執行間隔(必有的公式內容):FOR EACH ROW子句通知觸發器 每隔一行執行一次動作,而不是對整個表執行一次。
  6. <觸發的SQL語句>  --- 觸發器包含所要觸發的SQL語句:這裏的語句可以是任何合法的語句, 包括複合語句,但是這裏的語句受的限制和函數的一樣。當然,觸發SQL中可以調用“觸發了( INSERT | UPDATE | DELETE )觸發器的那一行數據”。

例如下方代碼:

create trigger add_stu
after insert on student for each row 
begin
      insert into student_score ( stu_id, score, rank)
        values( NEW.stuid, NEW.username);  -- NEW用來表示將要(BEFORE)或已經(AFTER)插入的新數據
end;

4. NEW 與 OLD關鍵字詳解

MySQL 中定義了 NEW 和 OLD,用來表示觸發器的所在表中,觸發了觸發器的那一行數據,來引用觸發器中發生變化的記錄內容,具體地:

  ① 在INSERT型觸發器中,NEW用來表示將要(BEFORE)或已經(AFTER)插入的新數據;

  ② 在UPDATE型觸發器中,OLD用來表示將要或已經被修改的原數據,NEW用來表示將要或已經修改爲的新數據;

  ③ 在DELETE型觸發器中,OLD用來表示將要或已經被刪除的原數據;


另外,原則上請編寫簡單高效的觸發執行語句,以免悄無聲息的浪費過多資源你還不知道!

 

三、實際應用


1. 數據準備

想我初三時常年倒數,成績穩定,因此我拿當時幾位老友排名數據來紀念一波(手動撓頭)。給大家提供一個測試數據;
(沿用的前兩篇“視圖”、“存儲過程”博文中的數據)

a.學生表

CREATE TABLE `student` (
  `ID`  int NOT NULL AUTO_INCREMENT  ,
  `NAME`  varchar(30) NOT NULL ,
  `SEX`  char(2) NOT NULL ,
  `AGE`  int NOT NULL ,
  `CLASS`  varchar(10) NOT NULL ,
  `GRADE`  varchar(20) NOT NULL ,
  `HOBBY`  varchar(100) NULL ,
 PRIMARY KEY (`ID`)
)

#插入數據:

INSERT INTO `student` (`ID`, `NAME`, `SEX`, `AGE`, `CLASS`, `GRADE`, `HOBBY`) VALUES ('1', '陳哈哈', '男', '15', '18班', '9年級', '上網');
INSERT INTO `student` (`ID`, `NAME`, `SEX`, `AGE`, `CLASS`, `GRADE`, `HOBBY`) VALUES ('2', '扈亞鵬', '男', '15', '18班', '9年級', '美食');
INSERT INTO `student` (`ID`, `NAME`, `SEX`, `AGE`, `CLASS`, `GRADE`, `HOBBY`) VALUES ('3', '劉曉莉', '女', '14', '18班', '9年級', '金希澈');
INSERT INTO `student` (`ID`, `NAME`, `SEX`, `AGE`, `CLASS`, `GRADE`, `HOBBY`) VALUES ('4', '朱志鵬', '男', '15', '18班', '9年級', '睡覺');
INSERT INTO `student` (`ID`, `NAME`, `SEX`, `AGE`, `CLASS`, `GRADE`, `HOBBY`) VALUES ('5', '徐立楠', '女', '14', '18班', '9年級', '閱讀');
INSERT INTO `student` (`ID`, `NAME`, `SEX`, `AGE`, `CLASS`, `GRADE`, `HOBBY`) VALUES ('6', '顧昊', '男', '15', '5班', '9年級', '籃球');
INSERT INTO `student` (`ID`, `NAME`, `SEX`, `AGE`, `CLASS`, `GRADE`, `HOBBY`) VALUES ('7', '陳子凝', '女', '15', '18班', '9年級', '看電影');

#插入結果:

b.成績表

CREATE TABLE `student_score` (
  `SID` int(11) NOT NULL,
  `S_NAME` varchar(30) NOT NULL,
  `TOTAL_SCORE` int(11) NOT NULL,
  `RANK` int(11) NOT NULL,
 PRIMARY KEY (`SID`)
) 

#插入數據:

INSERT INTO `student_score` (`SID`, `S_NAME`, `TOTAL_SCORE`, `RANK`) VALUES ('1', '陳哈哈', '405', '1760');
INSERT INTO `student_score` (`SID`, `S_NAME`, `TOTAL_SCORE`, `RANK`) VALUES ('2', '扈亞鵬', '497', '1000');
INSERT INTO `student_score` (`SID`, `S_NAME`, `TOTAL_SCORE`, `RANK`) VALUES ('3', '劉曉莉', '488', '1170');
INSERT INTO `student_score` (`SID`, `S_NAME`, `TOTAL_SCORE`, `RANK`) VALUES ('4', '朱志鵬', '405', '1770');
INSERT INTO `student_score` (`SID`, `S_NAME`, `TOTAL_SCORE`, `RANK`) VALUES ('5', '徐立楠', '530', '701');
INSERT INTO `student_score` (`SID`, `S_NAME`, `TOTAL_SCORE`, `RANK`) VALUES ('6', '顧昊', '485', '1286');
INSERT INTO `student_score` (`SID`, `S_NAME`, `TOTAL_SCORE`, `RANK`) VALUES ('7', '陳子凝', '704', '9');

#插入結果:


c.逃課上網表

CREATE TABLE `student_go_wangba` (
  `SID` int(9) NOT NULL,
  `SGW_NAME` varchar(30) DEFAULT NULL,
  `TIMES` int(9) DEFAULT NULL,
  PRIMARY KEY (`SID`)
) 

#插入數據:

INSERT INTO `student_go_wangba` (`SID`, `SGW_NAME`, `TIMES`) VALUES ('1', '陳哈哈', 15);
INSERT INTO `student_go_wangba` (`SID`, `SGW_NAME`, `TIMES`) VALUES ('2', '扈亞鵬', 1);
INSERT INTO `student_go_wangba` (`SID`, `SGW_NAME`, `TIMES`) VALUES ('3', '劉曉莉', 0);
INSERT INTO `student_go_wangba` (`SID`, `SGW_NAME`, `TIMES`) VALUES ('4', '朱志鵬', 63);
INSERT INTO `student_go_wangba` (`SID`, `SGW_NAME`, `TIMES`) VALUES ('5', '徐立楠', 0);
INSERT INTO `student_go_wangba` (`SID`, `SGW_NAME`, `TIMES`) VALUES ('6', '顧昊', 7);
INSERT INTO `student_go_wangba` (`SID`, `SGW_NAME`, `TIMES`) VALUES ('7', '陳子凝', 0);

#插入結果:

2. 使用案例

# Insert觸發器 - 級聯插入

       表數據:以上面的三張表爲例;學生表(student)、學生成績表(student_score)、逃課上網次數表(student_go_wangba),均已學號(stuid)爲主鍵。   

需求:

  • 需要設計一個觸發器A,當增加新的學生時,需要在成績表(student_score)中插入對應的學生信息,至於“分值、排名”字段爲0即可;後面由老師打分更新。
  • 需要設計一個觸發器B,當增加新的學生成績信息時,需要在逃課上網表(student_go_wangba)中插入對應的學生信息,至於“逃課上網次數”字段爲0即可;後面由教導主任“小平頭”去更新。(該觸發器意義在於:測試after insert鏈式反應是否支持)

 那麼,如何設計觸發器A呢?

  1. 首先它是一個插入Insert觸發器,是建立在表student上的;
  2. 然後是after,插入後的事件;
  3. 事件內容是插入成績表,需要插入學生的學號和姓名,number爲自增,而“分值、排名”目前不需要。
  • 注意:new表示student中新插入的值

 

觸發器A:

-- 新增觸發器A,當student表插入數據時,student_score表生成初始關聯數據
DROP TRIGGER IF EXISTS add_stu;
create trigger add_stu
after insert on student for each row 
begin
      INSERT INTO student_score (SID, S_NAME, TOTAL_SCORE, RANK) 
        VALUES (new.ID,new.NAME, 0, 0 );
end;

 

觸發器B:

-- 新增觸發器B,當student_score表插入數據時,student_go_wangba表生成初始關聯數據
DROP TRIGGER IF EXISTS add_score;
create trigger add_score
after insert on student_score for each row 
begin
      INSERT INTO student_go_wangba (SID, SGW_NAME, TIMES) 
        VALUES (new.SID,new.S_NAME, 0 );
end;

查詢一下我的觸發器:

show triggers \G

-- "\G"是幹什麼用的? 
-- 作用:在shell中樹形展示

如果在Navicat中就不用 \G,直接"show triggers;"就可以。 

show triggers;


執行觸發器,向student表中加入一條數據:

INSERT INTO `student` (`ID`, `NAME`, `SEX`, `AGE`, `CLASS`, `GRADE`, `HOBBY`) 
VALUES ('8', '李昂', '男', '15', '18班', '9年級', '看片兒');

結果如下圖所示:
 
同時插入三個數據,兩個觸發器正確執行了~
注意:創建觸發器和表一樣,建議增加判斷:DROP TRIGGER IF EXISTS `add_stu`;

# Delete觸發器 - 級聯刪除


表數據:以上面的三張表爲例;學生表(student)、學生成績表(student_score)、逃課上網次數表(student_go_wangba),均已學號(stuid)爲主鍵。  


需求:有些老是逃課上網的學生被開除,需要刪掉所有信息,以免給學校抹黑~~~  

  • 需要設計一個觸發器C,當刪除新的學生時,需要在成績表(student_score)中刪除對應的學生信息。
  • 需要設計一個觸發器D,當刪除新的學生成績信息時,需要在逃課上網表(student_go_wangba)中刪除對應的學生信息。(該觸發器意義在於:測試after delete鏈式反應是否支持)

那麼,如何設計觸發器C呢?

  1. 首先它是一個插入delete觸發器,是建立在表student上的;
  2. 然後是after,插入後的事件;
  3. 事件內容是關聯刪除成績表數據,需要刪除學生的學號即可。
  • 注意:old表示student中新刪除的值

觸發器C:

-- 新增觸發器C,當student表刪除數據時,student_score表刪除關聯數據
DROP TRIGGER IF EXISTS del_stu;
create trigger del_stu
after delete on student for each row 
begin
      DELETE FROM student_score where SID = old.ID;
end;

觸發器D:

-- 新增觸發器D,當student_score表刪除數據時,student_go_wangba表刪除關聯數據
DROP TRIGGER IF EXISTS del_score;
create trigger del_score
after delete on student_score for each row 
begin
      DELETE FROM student_go_wangba  where SID = old.SID;
end;

查詢一下我的觸發器(show triggers \G):

 

執行觸發器,從student表中刪除一條數據:

DELETE FROM `student` where NAME = '朱志鵬'

結果如下圖所示:

同時刪除三個數據,觸發器正確執行了。朱志鵬同學數據已經木得了~
注意:創建觸發器和表一樣,建議增加判斷:DROP TRIGGER IF EXISTS `del_stu`;

# Update觸發器 - 實時更新

跟Insert觸發器、Delete觸發器同理,這裏不再贅述

 

四、觸發器有哪些缺陷 @江湖中人

下面通過參考知乎、CSDN論壇、天涯社區,簡單講解幾個內容供大家參考:

問題一: 觸發器有哪些限制和注意事項

回答:        
        觸發器會有以下兩種限制:

  1. 觸發程序不能調用將數據返回客戶端的存儲程序,也不能使用採用CALL語句的動態SQL語句,但是允許存儲程序通過參數將數據返回觸發程序,也就是存儲過程或者函數通過OUT或者INOUT類型的參數將數據返回觸發器是可以的,但是不能調用直接返回數據的過程。
  2. 不能再觸發器中使用以顯示或隱式方式開始或結束事務的語句,如START TRANS-ACTION,COMMIT或ROLLBACK。

        注意事項:

        MySQL的觸發器是按照BEFORE觸發器、行操作、AFTER觸發器的順序執行的,其中任何一步發生錯誤都不會繼續執行剩下的操作,如果對事務表進行的操作,如果出現錯誤,那麼將會被回滾,如果是對非事務表進行操作,那麼就無法回滾了,數據可能會出錯。

 

問題二: 大型系統必須得要存儲過程和觸發器嗎?

回答1:   

     
我們先要弄清楚二個問題:

  1. 什麼是大型系統?
  2. 你討論的是什麼領域的應用,可以大致分爲二種:互聯網、企業內部

接下來給你舉一些例子:

  • SAP、peopleSoft、ERP等企業級別應用

        一般情況下,會使用存儲過程和觸發器,減少開發成本,畢竟其業務邏輯修改頻繁,而且爲通用,很多時候會把一些業務邏輯編寫成存儲過程,像Oracle會寫成包,比存儲過程更強大。
        另外一個原因是服務器的負載是可控,也即系統的訪問人數首先是可控的,沒有那麼大,而且這些數據又非常關鍵,爲此往往使用的設備也比較好,多用存儲櫃子支撐數據庫。
        

  • 另外一類互聯網行業的

        比如淘寶、知乎、微博等,數據庫的壓力是非常大的,也往往會最容易成爲瓶頸,而且多用PC服務器支撐,用戶量的增速是不可控的,同時在線訪問的用戶量也是不可控的,爲此肯定會把業務邏輯放到其他語言的代碼層,而且可以藉助一些LVS等類型軟硬件做負載均衡,以及平滑增減Web層的服務器,從而達到線性的增減而支持大規模的訪問。
        
        所以不管你的這個系統是否龐大,首先要分業務支持的對象,系統最可能容易出現瓶頸的地方在那?
        當然也不是說互聯網行業的應用就絕對不用存儲過程,這個也不對,曾在阿里做的Oracle遷移MySQL系統確實用了,因爲歷史的原因,另外還有一些新系統也有用,比如晚上進行定期的數據統計的一些操作,不過有量上的控制。存儲過程是好東西,要分場景,分業務類型來用就可以把握好。

回答2:

 

  • 肯定不能一刀切的說能用或者不能用,不同類型的系統、不同的規模、不同的歷史原因都會有不同的解決方案。
  • 一般情況下,Web應用的瓶頸常在DB上,所以會儘可能的減少DB做的事情,把耗時的服務做成Scale Out,這種情況下,肯定不會使用存儲過程;而如果只是一般的應用,DB沒有性能上的問題,在適當的場景下,也可以使用存儲過程。
  • 至於觸發器,我是知道有這東西但從來沒用過。我希望風險可控,遇到問題能夠快速的找到原因,儘可能不會去使用觸發器。

 

回答3:

  1. PLSQL可以大大降低parse/exec 百分比;
  2. 存儲過程可以自動完成靜態SQL variable bind;
  3. 存儲過程大大減少了JDBC網絡傳輸與交互,速度快;
  4. oracle 中存儲過程內部commit爲異步寫,一定程度上減少了等redo日誌落地時間;
  5. 存儲過程最大問題就是給數據庫開發工作壓力太大,另外架構升級時候會比較難解耦;
  6. 觸發器不推薦使用,觸發操作能在業務層解決就在業務層解決,否則很難維護,而且容易產生死鎖。

 

問題三: 爲什麼大家都不推薦使用MySQL觸發器而用存儲過程?

回答1:

  1. 存儲過程和觸發器二者是有很大的聯繫的,我的一般理解就是觸發器是一個隱藏的存儲過程,因爲它不需要參數,不需要顯示調用,往往在你不知情的情況下已經做了很多操作。從這個角度來說,由於是隱藏的,無形中增加了系統的複雜性,非DBA人員理解起來數據庫就會有困難,因爲它不執行根本感覺不到它的存在。
  2. 再有,涉及到複雜的邏輯的時候,觸發器的嵌套是避免不了的,如果再涉及幾個存儲過程,再加上事務等等,很容易出現死鎖現象,再調試的時候也會經常性的從一個觸發器轉到另外一個,級聯關係的不斷追溯,很容易使人頭大。其實,從性能上,觸發器並沒有提升多少性能,只是從代碼上來說,可能在coding的時候很容易實現業務,所以我的觀點是:摒棄觸發器!觸發器的功能基本都可以用存儲過程來實現。
  3. 在編碼中存儲過程顯示調用很容易閱讀代碼,觸發器隱式調用容易被忽略。
  4. 存儲過程的致命傷在於移植性,存儲過程不能跨庫移植,比如事先是在mysql數據庫的存儲過程,考慮性能要移植到oracle上面那麼所有的存儲過程都需要被重寫一遍。

 

回答2:
        這種東西只有在併發不高的項目,管理系統中用。如果是面向用戶的高併發應用,都不要使用。
        觸發器和存儲過程本身難以開發和維護,不能高效移植。

  • 觸發器完全可以用事務替代。
  • 存儲過程可以用後端腳本替代。

回答3:
        我覺得來自兩方面的因素:

  1. 存儲過程需要顯式調用,意思是閱讀源碼的時候你能知道存儲過程的存在,而觸發器必須在數據庫端才能看到,容易被忽略。
  2. Mysql的觸發器本身不是很好,比如after delete無法鏈式反應的問題。

        我認爲性能上其實還是觸發器佔優勢的,但是基於以上原因不受青睞。

五、總結

  • 觸發器是基於行觸發的,所以刪除、新增或者修改操作可能都會激活觸發器,所以不要編寫過於複雜的觸發器,也不要增加過得的觸發器,這樣會對數據的插入、修改或者刪除帶來比較嚴重的影響,同時也會帶來可移植性差的後果,所以在設計觸發器的時候一定要有所考慮。
  • 觸發器是一種特殊的存儲過程,它在插入,刪除或修改特定表中的數據時觸發執行,它比數據庫本身標準的功能有更精細和更復雜的數據控制能力。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章