1 引言html
Mysql的觸發器和存儲過程同樣,都是嵌入到mysql的一段程序。觸發器是mysql5新增的功能,目前線上鳳巢系統、北斗系統以及哥倫布系統使用的數據庫均是mysql5.0.45版本,不少程序好比fc-star管理端,sfrd(das),dorado都會用到觸發器程序,實現對於數據庫增、刪、改引發事件的關聯操做。本文介紹了觸發器的類型和基本使用方法,講述了觸發器使用中容易產生的誤區,從mysql源碼中獲得觸發器執行順序的結論,本文最後是實戰遭遇的觸發器經典案例。沒有特殊說明時,本文的實驗均基於mysql5.0.45版本。
2 Mysql觸發器的類型
2.1 Mysql觸發器的基本使用
建立觸發器。建立觸發器語法以下:
CREATE TRIGGER trigger_name trigger_time trigger_event
ON tbl_name FOR EACH ROW trigger_stmt
其中trigger_name標識觸發器名稱,用戶自行指定;
trigger_time標識觸發時機,用before和after替換;
trigger_event標識觸發事件,用insert,update和delete替換;
tbl_name標識創建觸發器的表名,即在哪張表上創建觸發器;
trigger_stmt是觸發器程序體;觸發器程序可使用begin和end做爲開始和結束,中間包含多條語句;
下面給出sfrd一個觸發器實例:
CREATE /*!50017 DEFINER = 'root'@'localhost' */ TRIGGER trig_useracct_update
AFTER UPDATE
ON SF_User.useracct FOR EACH ROW
BEGIN
IF OLD.ulevelid = 10101 OR OLD.ulevelid = 10104 THEN
IF NEW.ulevelid = 10101 OR NEW.ulevelid = 10104 THEN
if NEW.ustatid != OLD.ustatid OR NEW.exbudget != OLD.exbudget THEN
INSERT into FC_Output.fcevent set type = 2, tabid = 1, level = 1, userid = NEW.userid, ustatid = NEW.ustatid, exbudget = NEW.exbudget;
end if;
ELSE
INSERT into FC_Output.fcevent set type = 1, tabid = 1, level = 1, userid = NEW.userid, ustatid = NEW.ustatid, exbudget = NEW.exbudget;
END IF;
END IF;
END;
上述觸發器實例使用了OLD關鍵字和NEW關鍵字。OLD和NEW能夠引用觸發器所在表的某一列,在上述實例中,OLD.ulevelid表示表 SF_User.useracct修改以前ulevelid列的值,NEW.ulevelid表示表SF_User.useracct修改以後 ulevelid列的值。另外,若是是insert型觸發器,NEW.ulevelid也表示表SF_User.useracct新增行的 ulevelid列值;若是是delete型觸發器OLD.ulevelid也表示表SF_User.useracct刪除行的ulevelid列原值。
另外,OLD列是隻讀的,NEW列則能夠在觸發器程序中再次賦值。
上述實例也使用了IF,THEN ,ELSE,END IF等關鍵字。在觸發器程序體中,在beigin和end之間,可使用順序,判斷,循環等語句,實現通常程序須要的邏輯功能。
查看觸發器。查看觸發器語法以下,若是知道觸發器所在數據庫,以及觸發器名稱等具體信息:
SHOW TRIGGERS from SF_User like "usermaps%"; //查看SF_User庫上名稱和usermaps%匹配的觸發器
若是不瞭解觸發器的具體的信息,或者須要查看數據庫上全部觸發器,以下:
SHOW TRIGGERS; //查看全部觸發器
用上述方式查看觸發器能夠看到數據庫的全部觸發器,不過若是一個庫上的觸發器太多,因爲會刷屏,可能沒有辦法查看全部觸發器程序。這時,能夠採用以下方式:
Mysql中有一個information_schema.TRIGGERS表,存儲全部庫中的全部觸發器,desc information_schema. TRIGGERS,能夠看到表結構:
+----------------------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------------------------+--------------+------+-----+---------+-------+
| TRIGGER_CATALOG | varchar(512) | YES | | NULL | |
| TRIGGER_SCHEMA | varchar(64) | NO | | | |
| TRIGGER_NAME | varchar(64) | NO | | | |
| EVENT_MANIPULATION | varchar(6) | NO | | | |
| EVENT_OBJECT_CATALOG | varchar(512) | YES | | NULL | |
| EVENT_OBJECT_SCHEMA | varchar(64) | NO | | | |
| EVENT_OBJECT_TABLE | varchar(64) | NO | | | |
| ACTION_ORDER | bigint(4) | NO | | 0 | |
| ACTION_CONDITION | longtext | YES | | NULL | |
| ACTION_STATEMENT | longtext | NO | | | |
| ACTION_ORIENTATION | varchar(9) | NO | | | |
| ACTION_TIMING | varchar(6) | NO | | | |
| ACTION_REFERENCE_OLD_TABLE | varchar(64) | YES | | NULL | |
| ACTION_REFERENCE_NEW_TABLE | varchar(64) | YES | | NULL | |
| ACTION_REFERENCE_OLD_ROW | varchar(3) | NO | | | |
| ACTION_REFERENCE_NEW_ROW | varchar(3) | NO | | | |
| CREATED | datetime | YES | | NULL | |
| SQL_MODE | longtext | NO | | | |
| DEFINER | longtext | NO | | | |
+----------------------------+--------------+------+-----+---------+-------+
這樣,用戶就能夠按照本身的須要,查看觸發器,好比使用以下語句查看上述觸發器:
select * from information_schema. TRIGGERS where TRIGGER_NAME= 'trig_useracct_update'\G;
刪除觸發器。刪除觸發器語法以下:
DROP TRIGGER [schema_name.]trigger_name
2.2 Msyql觸發器的trigger_time和trigger_event
如今,從新注意到trigger_time和trigger_event,上文說過, trigger_time能夠用before和after替換,表示觸發器程序的執行在sql執行的前仍是後;trigger_event能夠用 insert,update,delete替換,表示觸發器程序在什麼類型的sql下會被觸發。
在一個表上最多創建6個觸發器,即1)before insert型,2)before update型,3)before delete型,4)after insert型,5)after update型,6)after delete型。
觸發器的一個限制是不能同時在一個表上創建2個相同類型的觸發器。這個限制的一個來源是觸發器程序體的「begin和end之間容許運行多個語句」(摘自mysql使用手冊)。
另外還有一點須要注意,msyql除了對insert,update,delete基本操做進行定義外,還定義了load data和replace語句,而load data和replace語句也能引發上述6中類型的觸發器的觸發。
Load data語句用於將一個文件裝入到一個數據表中,至關與一系列insert操做。replace語句通常來講和insert語句很像,只是在表中有 primary key和unique索引時,若是插入的數據和原來primary key和unique索引一致時,會先刪除原來的數據,而後增長一條新數據;也就是說,一條replace sql有時候等價於一條insert sql,有時候等價於一條delete sql加上一條insert sql。便是:
? Insert型觸發器:可能經過insert語句,load data語句,replace語句觸發;
? Update型觸發器:可能經過update語句觸發;
? Delete型觸發器:可能經過delete語句,replace語句觸發;
3 Mysql觸發器的執行順序
先拋出觸發器相關的幾個問題
3.1 若是before類型的觸發器程序執行失敗,sql會執行成功嗎?
實驗以下:
1)在FC_Word.planinfo中創建before觸發器:
DELIMITER |
create trigger trigger_before_planinfo_update
before update
ON FC_Word.planinfo FOR EACH ROW
BEGIN
insert into FC_Output.abc (planid) values (New.planid);
END
|
2)查看:mysql> select showprob from planinfo where planid=1;
+----------+
| showprob |
+----------+
| 2 |
+----------+
3)執行sql:
update planinfo set showprob=200 where planid=1; 觸發觸發器程序;
4)因爲不存在FC_Output.abc,before觸發器執行失敗,提示:
ERROR 1146 (42S02): Table 'FC_Output.abc' doesn't exist
5)再次查看:
mysql> select showprob from planinfo where planid=1;
+----------+
| showprob |
+----------+
| 2 |
+----------+
即修改sql未執行成功。即若是before觸發器執行失敗,sql也會執行失敗。
3.2 若是sql執行失敗,會執行after類型的觸發器程序嗎?
實驗以下:
1)在FC_Word.planinfo中創建after觸發器:
DELIMITER |
create trigger trigger_after_planinfo_update
after update
ON FC_Word.planinfo FOR EACH ROW
BEGIN
INSERT INTO FC_Output.fcevent set level = 2, type = 2, tabid = 5, userid = NEW.userid, planid = NEW.planid, planstat2 = NEW.planstat2, showprob = NEW.showprob, showrate = NEW.showrate, showfactor = NEW.showfactor, planmode = NEW.planmode;
END
|
2)查看觸發表:
mysql> select * from FC_Output.fcevent where planid=1;
Empty set (0.00 sec)
沒有planid=1的記錄
3)執行sql:
mysql> update planinfo set showprob1=200 where planid=1;
4)因爲不存在showprob1列,提示錯誤:
ERROR 1054 (42S22): Unknown column 'showprob1' in 'field list'
5)再次查看觸發表:
mysql> select * from FC_Output.fcevent where planid=1;
Empty set (0.00 sec)
觸發表中沒有planid=1的記錄,sql在執行失敗時,after型觸發器不會執行。
3.3 若是after類型的觸發器程序執行失敗,sql會回滾嗎?
實驗以下:
1)在FC_Word.planinfo中創建after觸發器:
DELIMITER |
create trigger trigger_after_planinfo_update
after update
ON FC_Word.planinfo FOR EACH ROW
BEGIN
insert into FC_Output.abc (planid) values (New.planid);
END
|
2)查看:mysql> select showprob from planinfo where planid=1;
+----------+
| showprob |
+----------+
| 2 |
+----------+
3)執行sql:
update planinfo set showprob=200 where planid=1;觸發觸發器程序;
4)因爲不存在FC_Output.abc,after觸發器執行失敗,提示:
ERROR 1146 (42S02): Table 'FC_Output.abc' doesn't exist
5)再次查看:
mysql> select showprob from planinfo where planid=1;
+----------+
| showprob |
+----------+
| 2 |
+----------+
即修改sql未執行成功。即若是after觸發器執行失敗,sql會回滾。
這裏須要說明一下,上述實驗所使用的mysql引擎是innodb,innodb引擎也是目前線上鳳巢系統、北斗系統以及哥倫布系統所使用的引擎,在 innodb上所創建的表是事務性表,也就是事務安全的。「對於事務性表,若是觸發程序失敗(以及由此致使的整個語句的失敗),該語句所執行的全部更改將回滾。對於非事務性表,不能執行這類回滾」(摘自mysql使用手冊)。於是,即便語句失敗,失敗以前所做的任何更改依然有效,也就是說,對於 innodb引擎上的數據表,若是觸發器中的sql或引起觸發器的sql執行失效,則事務回滾,全部操做會失效。
3.4 mysql觸發器程序執行的順序
當一個表既有before類型的觸發器,又有after類型的觸發器時;當一條sql語句涉及多個表的update時,sql、觸發器的執行順序通過mysql源碼包裝過,有時比較複雜。
能夠先看一段mysql的源代碼,當SQL中update多表的時候,Mysql的執行過程以下(省去了無關代碼):
/* 遍歷要更新的全部表 */
for (cur_table= update_tables; cur_table; cur_table= cur_table->next_local)
{
org_updated = updated
/* 若是有 BEFORE 觸發器,則執行;若是執行失敗,跳到err2位置 */
if (table->triggers &&
table->triggers->process_triggers(thd, TRG_EVENT_UPDATE,TRG_ACTION_BEFORE, TRUE))
goto err2;
/*執行更新,若是更新失敗,跳到err位置*/
if(local_error=table->file->update_row(table->record[1], table->record[0])))
goto err;
updated++; // 更新計數器
/* 若是有 AFTER 觸發器,則執行;若是執行失敗,跳到err2位置*/
if (table->triggers &&
table->triggers->process_triggers(thd, TRG_EVENT_UPDATE, TRG_ACTION_AFTER, TRUE))
goto err2;
err:
{
/*標誌錯誤信息,寫日誌等*/
}
err2:
{
/*恢復執行過的操做*/
check_opt_it.rewind();
/*若是執行了更新,且表是有事務的,作標誌*/
if (updated != org_updated)
{
if (table->file->has_transactions())
transactional_tables= 1;
}
}
}
從上面代碼能夠找到本章開始時拋出問題的答案。
1) 若是before型觸發器執行失敗,直接goto跳到err2位置,不會執行後續sql語句;
2) 若是sql執行失敗,直接goto跳到err位置,不會執行或許的after型觸發器;
3) 如過after觸發器執行失敗,goto到err2位置,恢復執行過的操做,且在事務型的表上作標記。
另外,在使用複雜的sql時,因爲有些複雜的sql是mysql本身定義的,因此存在不肯定性,使用簡單的sql比較可控。
4 Mysql觸發器在數據庫同步中的表現
4.1 觸發器運行失敗時,數據庫同步會失敗嗎?
有同步關係以下dbA?dbB。初始時同步正常。
1)在dbB上創建觸發器:
DELIMITER |
create trigger trigger_after_planinfo_update
after update
ON FC_Word.planinfo FOR EACH ROW
BEGIN
insert into FC_Output.abc (planid) values (New.planid);
END
|
2)在dbA上執行sql,執行成功;
mysql>
update planinfo set showprob=200 where planid= 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
3)因爲dbB上沒有FC_Output.abc表,觸發器會執行失敗,這時,檢查一下同步狀態:
Slave_IO_Running: Yes
Slave_SQL_Running: NO
Last_Errno: 1146
Last_Error: Error 'Table 'FC_Output.abc' doesn't exist' on query. Default database: 'FC_Word'. Query: 'update planinfo set showprob=200 where planid= 1'
能夠看到IO線程運行正常,sql線程運行失敗,並提示觸發器運行失敗的錯誤信息。
回憶一下3.1和3.3所述部分,不管是before部分的觸發器仍是after類型的觸發器,對於innodb引擎,當觸發器執行失敗時,相應sql也會執行失敗,因此數據庫同步也會失敗。
4.2 建立、刪除觸發器寫bin-log
建立和刪除觸發器的語句也會寫入bin-log裏,因此也會如通常的insert,update,delete語句同樣同步到下游數據庫中,即上游建立觸發器,下游也會建立。
這裏再引出兩個小問題:有同步關係dbA?dbB,
1) 在dbA上建立一個觸發器,若是dbB上已經有同表同類型的觸發器,同步狀態如何?
2) 在dbB上刪除一個觸發器,若是dbB上沒有對應觸發器,同步狀態如何?
這兩個問題能夠類比同步中的insert語句和delete語句,答案就是
1) 同步失敗,由於不容許重複建立同表同類型的觸發器;
2) 同步正常,由於drop一個不存在的觸發器,不影響運行結果;
5 Mysql觸發器經典案例
5.1 案例1 一條sql涉及多個表的update時,觸發獲得update以前的舊值
【現象】表test_info上建有觸發器以下:
CREATE /*!50017 DEFINER = 'root'@'localhost' */ TRIGGER trig_test_info_update
AFTER UPDATE
ON FC_Word.test_info FOR EACH ROW
BEGIN
DECLARE tlevel INTEGER DEFAULT 0;
DECLARE ttype INTEGER DEFAULT 0;
SET tlevel = 4;
SET ttype = 33;
INSERT INTO TEST_Output.fcevent (te, le, uid, pid, uid, wid, bi, mbid, wl) SELECT ttype, tlevel, NEW.uid, NEW.pid, NEW.uid, NEW.wid, NEW.bi, NEW.mbid, wl FROM TEST_Word.wext2 where wid = NEW.wid;
/*。。。其他部分邏輯省略*/
END IF;
END;
這個觸發器程序有點長,能夠單看飄黃的兩句,即更新操做知足第一個條件執行飄黃語句時,觸發器的行爲。觸發器是創建在test_info表上的,飄黃語句中能夠看到,也須要查詢wext2表。
執行以下sql1:
Update test_info a, wext2 b set a.th=(a.th+1), a.w4=(a.w4&8), b.wl=NULL where a.wid=b.wid and a.wid=142394379;
能夠看到sql中既修改了test_info2表,同時修改了wext2表,程序原意是觸發獲得wext2表wl字段修改後的新值(即NULL);不過實驗獲得,執行上述sql後,觸發器程序查詢到的wurl是sql修改以前的舊值。
再執行下面相似sql2:
Update wext2 a, test_info2 b set b.th=(b.th+1), b.w4=(b.w4&8), a.wl=NULL where a.wid=b.wid and a.wid=142394379;
實驗的到,執行上述sql後,觸發器程序查詢到的wurl是sql修改以後的新值。
【緣由】緣由固然與sql中的別名a,b無關,而是和wext2表和test_info表的書寫順序有關。如本文3.4部分所述,一條sql涉及多個表的 update操做時,數據表字段、觸發器執行順序是mysql源碼包裝過的。在執行上述sql1時,先執行test_info的更新,而後是after觸發器,最後是wext2的更新,也就是說,在執行after觸發器時,wext2尚未進行更新,因此觸發獲得的是舊值。而執行sql2時,先執行 wext2更新,而後是test_info更新,最後是after觸發器,也就是說,在執行after觸發器時,wext2已經更新完畢,因此出去獲得的是新值。
引發上述現象是順序關係的,不管該表是否支持事務。在使用複雜的sql時,因爲有些複雜的sql是mysql本身定義的,因此存在不肯定性,存在風險,使用簡單的sql比較可控。
5.2 案例2 mysql5.0.19版本修改表結構後觸發器失效
【現象】userpref表上建有after類型觸發器,修改userpref表的外鍵關聯後,在userpref表中的新增記錄沒有觸發下來,即觸發器失效。
【緣由】mysql5.0.19修改表結構是,觸發器消失。這是mysql5.0.19的一個bug,在建立觸發器時,會把觸發器的內容保存在 information_schema.TRIGGERS表中,同時在var目錄下建立觸發器的數據庫目錄下建立一個觸發器名稱爲前綴,以TRN爲後綴的文件,當修改觸發器的表時,information_schema.TRIGGERS表的內容會刪除,致使觸發器消失。
在mysql5.0.45版本中,這個bug已經被修復。Mysql5.0.45版本的觸發器,不管是修改表的索引、外鍵,仍是改變表字段,觸發器都不會失效。
5.3 案例3 刪除數據表後觸發器失效
【現象】聯調環境中存在dbA?dbB,主庫dbA上沒有觸發器,在從庫dbB上的FC_Word.wnegative表,FC_Word.wbuget 表上建有觸發器;觸發器開始運行正常,期間沒有對從庫的任何直接操做,有一日發現對wnegative表上的修改沒法觸發。查看從庫狀態,同步正常;用 select TRIGGER_NAME from information_schema.TRIGGERS發現wnegative表上的觸發器消失了;在var/FC_Word目錄下也沒有 wnegative的.TRN文件,wnegative表上的觸發器不見了。
【分析】查找dbB的查詢日誌,發現有一條:
100223 18:27:45 135939 Query DROP TABLE IF EXISTS `wnegative`
135939 Query CREATE TABLE `wnegative` (
KEY `Index_wnegative_planid` (`planid`),
KEY `Index_wnegative_unitid` (`unitid`)
135939 Query /*!40000 ALTER TABLE `wnegative` DISABLE KEYS */
100223 18:27:46 135939 Query INSERT INTO `wnegative` VALUES (614,1,289026,2911155,1848481);
能夠看到,在100223 18:27:45時,刪除了表wnegative,緊接着有建立表wnegative;查找觸發表發現,在100223 18:27:45時間後對wnegative的修改就沒有觸發了,而在這個以前對wnegative的修改是觸發正常的。故,懷疑對wnegative表的刪除使wnegative表上的觸發器也被刪除。對wnegative表的刪除是在主庫dbA上操做後,被同步到dbB上。
【緣由】在刪除wnegative表時,mysql同時刪除了wegative表上的觸發器。
能夠經過下面實驗證實上述猜想:
1) 首先在wnegative創建after insert型觸發器;
2) 增長一條wnegative中記錄;
3) 查看結果發現觸發器正確觸發;
4) 刪除wnegative表;
5) 使用select TRIGGER_NAME from information_schema.TRIGGERS查看全部觸發器,wnegative表上觸發器已經不存在了;同時到var/FC_Word目錄下,對應觸發器的.TRN文件也不存在了;
6) 從新建立wnegative表,並增長一條wnegative中記錄;沒有了wnegative表上觸發器,天然也不能觸發任何結果。
6 結束語
Mysql中的觸發器功能已經在鳳巢系統的各個模塊中有普遍應用,究其細節,還有不少值得注意的地方;本文創建在實驗和案例的基礎上,數據庫基於線上系統使用的mysql5.0.45版本,分析了觸發器相關的一些特殊狀況下msyql的處理方式。
(全文完)