1. 程式人生 > >【轉】mysql觸發器的實戰經驗(觸發器執行失敗,sql會回滾嗎)

【轉】mysql觸發器的實戰經驗(觸發器執行失敗,sql會回滾嗎)

1   引言

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;
}
}