🛢️ 第15章 触发器
📄 一句话概括

DROP TRIGGER 名 和 DROP TRIGGER IF EXISTS 名 是两种写法,后者能跳过不存在的名字。 删完必须回头验一次:删错名字不会报错,日志表却悄悄停了。 DROP TABLE 会把表上的触发器一起带走,一条都不留。

📝 本章和前面几章的关系

教材 15.4 只有一句话:DROP TRIGGER 加名字。这一篇补四件事:写法、验证、排查、连带。 这一篇是第 15 章的收尾,后面 15.5 小结和 15.6 实践与练习并进本篇末尾。 上一篇讲用,这一篇讲删。 触发器最大的麻烦是「代码里搜不到它」——所以删之前那一步查证,比删本身重要得多。

一、三种写法,两种通

写法 结果
DROP TRIGGER dz 成功
DROP TRIGGER IF EXISTS dz2 成功
DROP TRIGGER IF EXISTS nope_zzz 成功(跳过)
DROP TRIGGER nope_zzz no such trigger: nope_zzz
DROP TRIGGER dz3 ON tb_g near "ON": syntax error

第四行不带 IF EXISTS 删一个不存在的名字,报 no such trigger——这是好事, 说明它没假装成功。 第五行是 MySQL 的写法:它的 DROP TRIGGER 语法里必须带 ON 表名, 用来定位这张表上的触发器。 SQLite 不需要这一步,所以多出来的 ON 表名 直接语法错。 IF EXISTS 不是可有可无的装饰。 部署脚本里删一个「可能已经删过」的触发器,不带它第二次执行就中断。 代价是它会把「名字拼错了」也一起吞掉。 所以删完必须回头查一遍,别只看有没有报错。

二、删完要验:动作真的停了

光看「删除成功」不够,得看它原来干的活是不是真的停了。

时机 商品表行数 日志表行数
删之前插一行 3 1
删之后插一行 4 1

表里的行照旧在涨,日志表停在原地——触发器确实不干活了。 这一步必须验。删错名字时一句报错都没有,日志表却悄悄停了,这种故障要等到对账才对得出来。 验的办法很简单:删之前插一行看日志涨不涨,删之后再插一行看它涨不涨。 两次都插、两次都查,两分钟的事,能省掉后面几天的排查。

三、删之前先查:这张表上挂了几个

sqlite_master 的 tbl_name 列就是干这个用的。 想清一张表上的所有触发器,先按 tbl_name 筛一遍。

SELECT name, tbl_name
  FROM sqlite_master
 WHERE type = 'trigger';

-- 只看某一张表上的
SELECT name
  FROM sqlite_master
 WHERE type = 'trigger'
   AND tbl_name = 'tb_g';

这一步的意义不只是「拿到名单」,更是「知道自己漏了什么」。 人工删的时候最常见的失误不是删错,是漏删:以为只有两个,实际挂了四个,剩下那两个继续跑, 出问题时谁都想不到它们还在。

四、批量删:拿到名单再动手

做法是先把名字查出来,再一个个删——不靠记忆、不写死名单。 故意把 t_nope 混在名单末尾:前三个删掉,最后一个报 no such trigger 被接住。

名字 结果
t_del 已删除
t_ins 已删除
t_upd 已删除
t_nope no such trigger: t_nope

这就是「跳过不存在的、删掉该删的」,脚本可以反复跑而不中断。 删完再看一眼:tb_g 上一个不剩,tb_h 上那个没被碰。 注意 tb_h 那个没被碰——这正是按 tbl_name 筛的价值。 如果图省事写成「删掉所有触发器」,另一张表上的守卫就一起没了,而且它也是静默消失的。

五、删表会连触发器一起带走

触发器是挂在表上的,表没了它没有立足之地,数据库会顺手清掉。

时机 触发器数
删表之前 2
删表之后 0

这两条触发器谁都没显式删过,跟着表一起消失了。 知道这一点有两面:手工删表时不用担心留下孤儿触发器; 但反过来也危险——删表是个「连带动作很大」的操作,比看上去重。 表上挂的触发器越多,删表的连带影响越大。 所以删表之前那一步「查有哪些触发器」,其实是在估这次操作的爆炸半径。

六、本章小结与实践与练习

第 15 章四篇,真正要带走的是五条: 一、触发器由时机、事件、表三样定位,六种组合;要「拦」用 BEFORE,要「记」用 AFTER。 二、OLD 和 NEW 不是什么时候都有,SQLite 建的时候不查、用的时候才炸。 三、INSTEAD OF 只给视图,MySQL 没有这个时机;UPDATE OF 列名 也是 SQLite 独有。 四、多个触发器的执行顺序两边相反(SQLite 后建先跑,MySQL 先建先跑),别靠它保证结果。 五、冗余汇总必须「先对齐基线、之后全走触发器、再配一个对账兜底」,三步缺一不可。 这一章最该记住的一句是:触发器是「看不见的代码」。 过程要有人 CALL,视图要有人 SELECT,触发器什么都不用——写表的时候它自己就动了。 好处是不会漏,坏处是排查时想不起来它存在。 所以建它的那天,就要把「它叫什么、挂在哪、干什么」写进文档; 删它的那天,要按本节这套流程查一遍、验一遍。 15.6 的练习可以挑这三道: 给商品表加一个「价格不许往下调」的 BEFORE UPDATE 守卫,用 WHEN 限定条件; 用一个 AFTER INSERT 触发器维护「每个分类的商品数」汇总表,装完记得做一次全量重算; 把一张表上的全部触发器导出成一段 CREATE TRIGGER 脚本,再在空库里回放一遍。 三道都动手做过,这一章就算真读完了。

七、动手 Demo:删之前、删之后、批量删、删表

这一段把删除的四件事跑一遍:三种 DROP 写法各试一次(含 MySQL 的 ON 表名); 删之前插一行、删之后再插一行,看日志是不是真的停了; 按 tbl_name 筛出名单再批量删,末尾故意混一个不存在的名字;最后看 DROP TABLE 的连带效果。

import sqlite3

c = sqlite3.connect(u":memory:", isolation_level=None)
c.executescript(u"""
CREATE TABLE tb_g (id INTEGER PRIMARY KEY AUTOINCREMENT,
                    name TEXT, price REAL, stock INTEGER);
CREATE TABLE tb_log (id INTEGER PRIMARY KEY AUTOINCREMENT, act TEXT);
INSERT INTO tb_g(name, price, stock) VALUES ('机械键盘', 399.0, 12);
INSERT INTO tb_g(name, price, stock) VALUES ('无线鼠标', 129.0, 30);
""")

# ① 三种写法
c.execute(u"CREATE TRIGGER dz AFTER INSERT ON tb_g "
          u"FOR EACH ROW BEGIN SELECT 1; END")
c.execute(u"DROP TRIGGER dz")                        # 通
c.execute(u"DROP TRIGGER IF EXISTS nope_zzz")         # 通,跳过
try:
    c.execute(u"DROP TRIGGER nope_zzz")                # 报 no such trigger
except sqlite3.Error as e:
    print(e)
try:
    c.execute(u"DROP TRIGGER dz2 ON tb_g")             # MySQL 写法,语法错
except sqlite3.Error as e:
    print(e)

# ② 删完要验:动作是不是真停了
c.execute(u"CREATE TRIGGER trg_ins AFTER INSERT ON tb_g "
          u"FOR EACH ROW BEGIN INSERT INTO tb_log(act) VALUES ('ins'); END")
c.execute(u"INSERT INTO tb_g(name, price, stock) VALUES ('丙', 30.0, 1)")
print(u"删之前日志", c.execute(u"SELECT count(*) FROM tb_log").fetchone()[0])
c.execute(u"DROP TRIGGER trg_ins")
c.execute(u"INSERT INTO tb_g(name, price, stock) VALUES ('丁', 40.0, 1)")
print(u"删之后日志", c.execute(u"SELECT count(*) FROM tb_log").fetchone()[0])

# ③ 按 tbl_name 筛名单,批量删
for nm, ev in ((u"t_ins", u"INSERT"), (u"t_upd", u"UPDATE"), (u"t_del", u"DELETE")):
    c.execute(u"CREATE TRIGGER %s AFTER %s ON tb_g "
              u"FOR EACH ROW BEGIN SELECT 1; END" % (nm, ev))
c.execute(u"CREATE TABLE tb_h (id INTEGER PRIMARY KEY, v REAL)")
c.execute(u"CREATE TRIGGER h_ins AFTER INSERT ON tb_h "
          u"FOR EACH ROW BEGIN SELECT 1; END")
print(c.execute(u"SELECT name FROM sqlite_master WHERE type = 'trigger' "
                u"AND tbl_name = 'tb_g' ORDER BY name").fetchall())
for nm in [u"t_del", u"t_ins", u"t_upd", u"t_nope"]:
    try:
        c.execute(u"DROP TRIGGER " + nm)
        print(nm, u"已删除")
    except sqlite3.Error as e:
        print(nm, e)

# ④ 删表连带
c.execute(u"CREATE TRIGGER g1 AFTER INSERT ON tb_g FOR EACH ROW BEGIN SELECT 1; END")
print(u"删表前", c.execute(u"SELECT count(*) FROM sqlite_master "
                         u"WHERE type = 'trigger'").fetchone()[0])
c.execute(u"DROP TABLE tb_g")
print(u"删表后", c.execute(u"SELECT count(*) FROM sqlite_master "
                         u"WHERE type = 'trigger'").fetchone()[0])

实跑输出:

--------------------------------------------------------------------
删除触发器:删完记得回头看一眼
--------------------------------------------------------------------


  三种写法,两种通
  --------------------------------------------------------------------

  写法                               结果
  ------------------------------------------------------------
  `DROP TRIGGER dz`                  成功
  `DROP TRIGGER IF EXISTS dz2`       成功
  `DROP TRIGGER IF EXISTS nope_zzz`  成功
  `DROP TRIGGER nope_zzz`            no such trigger: nope_zzz
  `DROP TRIGGER dz3 ON tb_g`         near "ON": syntax error

**前三行通,后两行不通。**
**第四行不带 `IF EXISTS` 删一个不存在的名字,报 `no such trigger`——这是好事,说明它没假装成功。**
**第五行是 MySQL 的写法,`ON 表名` 那段在 SQLite 里是多余的,报 `near "ON": syntax error`。**

**`IF EXISTS` 不是可有可无的装饰。**
**部署脚本里删一个「可能已经删过」的触发器,不带它第二次执行就中断。**
**代价是它会把「名字拼错了」也一起吞掉。**
**所以删完必须回头查一遍,别只看有没有报错。**

  删掉之后,动作真的停了
  --------------------------------------------------------------------
**光看「删除成功」不够,得看它原来干的活是不是真的停了。**

  时机          tb_g 行数  tb_log 行数
  ------------------------------------
  删之前插一行  3          1
  删之后插一行  4          1

**表里的行照旧在涨,日志表停在原地——触发器确实不干活了。**
**这一步必须验:删错名字的时候一句报错都没有,日志表却悄悄停了,这种故障要等到对账才对得出来。**

  删之前先查:这张表上挂了几个
  --------------------------------------------------------------------
**`sqlite_master` 的 `tbl_name` 列就是干这个用的。想清一张表上的所有触发器,先筛一遍。**

  mysql> SELECT name, tbl_name FROM sqlite_master WHERE type = 'trigger'
  name  tbl_name
  --------------
  t_ins tb_g
  t_upd tb_g
  t_del tb_g
  h_ins tb_h
  4 rows in set

  mysql> 只筛 tb_g 上的:

  mysql> SELECT name FROM sqlite_master WHERE type = 'trigger' AND tbl_name = 'tb_g'
  name
  -----
  t_ins
  t_upd
  t_del
  3 rows in set

  批量删:拿到名单再动手
  --------------------------------------------------------------------
**实际运维里经常要「把这张表上的触发器全清掉」。**
**做法是先把名字查出来,再一个个删——不靠记忆、不写死名单。**

  查到 3 个:t_del、t_ins、t_upd

  名字    结果
  -------------------------------
  t_del   已删除
  t_ins   已删除
  t_upd   已删除
  t_nope  no such trigger: t_nope

**故意把 `t_nope` 混在名单末尾:前三个删掉,最后一个报 `no such trigger` 被接住。**
**这就是「跳过不存在的、删掉该删的」,脚本可以反复跑而不中断。**
**再看剩下的:`tb_g` 上一个不剩,`tb_h` 上那个没被碰。**

  mysql> SELECT name, tbl_name FROM sqlite_master WHERE type = 'trigger'
  name  tbl_name
  --------------
  h_ins tb_h
  1 row in set

  删表会连触发器一起带走
  --------------------------------------------------------------------
**触发器是挂在表上的,表没了它没有立足之地,数据库会顺手清掉。**

  时机      触发器数
  ------------------
  删表之前  2
  删表之后  0

**这两条触发器谁都没显式删过,跟着表一起消失了。**
**知道这一点有用:手工删表的时候不用担心留下孤儿触发器。**
**反过来也危险——删表是个「连带动作很大」的操作,比看上去重。**

**教材 15.4 这一节就三句话:`DROP TRIGGER` 加名字。**
**真正要在工程里用起来的,是本节这四件事:写法分两种、删完要验、删前先查、删表会连带。**

第一节五行报错里,no such trigger 和 near "ON": syntax error 是两回事。 前者是「语法对、名字不存在」,后者是「这条语法在这儿不成立」。 IF EXISTS 只吞前者,吞不掉后者。 第二节两行数字:删之前 1 条,删之后还是 1 条。 商品表从 3 行涨到 4 行,日志表一动不动——这才是「删掉了」的证据。 只看 DROP 成功,看不到这一层。 第三节最后那个 t_nope 报错被接住,脚本继续往下走,这就是「可反复执行的脚本」该有的样子。 而且注意 tb_h 上那个 h_ins 全程没被碰——按 tbl_name 筛选保住了它。 第四节两行 2 → 0:两条触发器谁都没显式删,跟着表一起没了。 删表之前数一下有几个触发器,等于在估这次操作会带走多少东西。

30 秒实验

  1. 把 DROP TRIGGER IF EXISTS 换成 DROP TRIGGER,再删一个不存在的名字, 看脚本是不是真的中断——把后面的语句接在同一段脚本里就能看出来。
  2. 写一个循环:从 sqlite_master 读出所有触发器的 sql,逐条打印。 再把这些 sql 在空库里执行一遍,看能不能完整还原——还原不了的,就是漏了依赖的表。
  3. 建一张表挂三个触发器,然后 DROP TABLE,比较删表前后的 sqlite_master 行数, 顺便确认剩下的对象(表、视图、索引)有没有被连带。

常见误区

  1. 以为 DROP TRIGGER 成功就等于「不会再触发了」。删错名字时一句报错都没有。 正确做法是删完再插一行,看该记的日志有没有记。
  2. 图省事写成「删掉库里所有触发器」。别的表上那些守卫会一起消失,而且同样静默。 要按 tbl_name 筛,只动目标表。
  3. 以为删表会留下孤儿触发器。反过来,删表会连触发器一起清掉。 真正该注意的是「删表带走了多少东西」这件事本身。
自测三问(点开对答案) 1. `DROP TRIGGER IF EXISTS` 有什么风险,怎么补? —— 风险是它会把「名字拼错了」也静默吞掉,看起来成功、实际什么都没删。 补法是删完回头查一次 `sqlite_master`,并插一行数据验证触发的动作确实停了。 2. 为什么批量删之前要先按 `tbl_name` 查一遍? —— 一是不靠记忆拿名单,二是避免误删别的表上的触发器。 「删掉所有触发器」和「删掉这张表上的触发器」听起来差不多,影响面差很多。 3. 删掉一张表,表上的触发器还在吗? —— 不在。触发器挂在表上,表没了它就没有立足之处,会跟着一起被清掉。 所以删表之前数一下有几个触发器,是在估这次操作的连带影响。

第 15 章到这里结束。 下一篇进入第 16 章:事务——从「自动提交」讲到「回滚点」,再到锁机制和隔离级别。

相关笔记