Table of Contents
本章提供有关编写触发器函数的一般信息。触发器函数可以用大多数可用的过程语言编写,包括 PL/pgSQL(Chapter 41)、 PL/Tcl(Chapter 42)、 PL/Perl(Chapter 43)和 PL/Python(Chapter 44)。读完本章后,你还应查阅所用过程语言对应的章节,了解用该语言编写触发器的语言相关细节。
也可以用 C 编写触发器函数,不过大多数人会觉得使用某种过程语言更容易。目前还不能用普通 SQL 函数语言编写触发器函数。
触发器是一种规定:每当执行某种类型的操作时,数据库就自动执行某个特定函数。触发器可以附加到表(无论是否分区)、视图和外部表上。
在表和外部表上,触发器可以定义为在任何 INSERT、 UPDATE 或 DELETE 操作之前或之后执行; 既可以对每个被修改的行执行一次,也可以对每个 SQL 语句执行一次。 UPDATE 触发器还可以设置成只有在 UPDATE 语句的 SET 子句中提到特定列时才触发。触发器也可以针对 TRUNCATE 语句触发。一旦发生触发器事件,就会在适当的时机调用该触发器的函数来处理该事件。
在视图上,触发器可以定义为代替 INSERT、 UPDATE 或 DELETE 操作执行。这种 INSTEAD OF 触发器会对视图中每个需要修改的行触发一次。触发器函数负责对视图底层的基表执行必要的修改,并在适当时返回该修改后的行,使其在视图中呈现为应有的样子。视图上的触发器也可以定义为在 INSERT、UPDATE 或 DELETE 操作之前或之后,对每个 SQL 语句执行一次。不过,这种触发器只有在该视图上还存在一个 INSTEAD OF 触发器时才会触发。否则,任何以该视图为目标的语句都必须被重写成作用于其底层基表的语句,此时触发的将是附加在那些基表上的触发器。
必须先定义触发器函数,之后才能创建触发器本身。触发器函数必须声明为不接受参数并返回类型 trigger 的函数。(触发器函数通过特殊传入的 TriggerData 结构接收输入,而不是通过普通函数参数。)
创建出合适的触发器函数后,就可以使用 CREATE TRIGGER 创建触发器。同一个触发器函数可以供多个触发器使用。
PostgreSQL 同时提供 每行 触发器和 每语句 触发器。对于每行触发器,触发它的语句所影响的每一行都会调用一次触发器函数。相反,每语句触发器只会在执行相应语句时调用一次,不管该语句影响了多少行。特别是,即使某个语句一行也没有影响,仍会执行所有适用的每语句触发器。这两类触发器有时也分别称为 行级 触发器和 语句级 触发器。 TRUNCATE 的触发器只能定义为语句级,不能定义为每行。
触发器还可以按照它们是在操作 之前、 之后,还是 取代 该操作而触发来分类。这些分别称为 BEFORE 触发器、 AFTER 触发器和 INSTEAD OF 触发器。语句级 BEFORE 触发器自然会在语句开始执行任何操作之前触发,而语句级 AFTER 触发器则在语句末尾触发。这些类型的触发器可以定义在表、视图或外部表上。行级 BEFORE 触发器会在对某一行执行操作之前立即触发,而行级 AFTER 触发器会在语句结束时触发(但早于任何语句级 AFTER 触发器)。这些类型的触发器只能定义在表和外部表上,不能定义在视图上。INSTEAD OF 触发器只能定义在视图上,而且只能是行级;当视图中的每一行被识别为需要执行操作时,它们就会立即触发。
如果触发器被定义为 约束触发器,则 AFTER 触发器的执行可被推迟到事务末尾,而不是语句末尾。 无论哪种情况,触发器都作为触发它的语句所属同一事务的一部分执行,因此只要语句或触发器任一方发生错误,二者的效果都会回滚。此外,触发器总是以将该触发事件排入队列的角色身份运行,除非触发器函数标记为 SECURITY DEFINER,那样的话则以函数所有者身份运行。
若 INSERT 包含 ON CONFLICT DO UPDATE 子句,则被触发的行上可能先执行行级 BEFORE INSERT 触发器,再执行 BEFORE UPDATE 触发器。若触发器不是 幂等的,这种交互可能很复杂,因为 BEFORE INSERT 触发器所做更改会被 BEFORE UPDATE 触发器看到, 包括对 EXCLUDED 列的更改。
注意,当指定 ON CONFLICT DO UPDATE 时,语句级 UPDATE 触发器会执行,不管 UPDATE 是否实际影响了行(也不管是否走到了 UPDATE 分支)。带有 ON CONFLICT DO UPDATE 的 INSERT 会先执行语句级 BEFORE INSERT 触发器,再执行语句级 BEFORE UPDATE 触发器,然后执行语句级 AFTER UPDATE 触发器,最后执行语句级 AFTER INSERT 触发器。
如果某条语句以继承层次或分区层次中的父表为目标,它不会导致受影响子表上的语句级触发器触发;只有父表上的语句级触发器会触发。不过,任何受影响子表上的行级触发器仍会触发。
如果对分区表执行 UPDATE 导致某行移动到另一个分区,这将表现为从原始分区执行一次 DELETE,然后向新分区执行一次 INSERT。在这种情况下,原始分区上的所有行级 BEFORE UPDATE 触发器和所有行级 BEFORE DELETE 触发器都会触发。随后,目标分区上的所有行级 BEFORE INSERT 触发器都会触发。当这些触发器都会影响被移动的那一行时,应考虑到可能出现令人意外的结果。对于 AFTER ROW 触发器,会执行 AFTER DELETE 和 AFTER INSERT 触发器;但不会执行 AFTER UPDATE 触发器,因为该 UPDATE 已被转换成一次 DELETE 和一次 INSERT。对于语句级触发器而言,即使发生了行移动,也不会触发任何 DELETE 或 INSERT 触发器;只会触发在 UPDATE 语句所用目标表上定义的 UPDATE 触发器。
由每语句触发器调用的触发器函数应始终返回 NULL。由每行触发器调用的触发器函数则可以根据需要向调用它的执行器返回一个表行(即类型 HeapTuple 的值)。在操作之前触发的行级触发器可以作出如下选择:
它可以返回 NULL,以跳过对当前行的操作。这会指示执行器不要执行引发该触发器的行级操作(即对某个具体表行的插入、修改或删除)。
仅对于行级 INSERT 和 UPDATE 触发器,返回的行会成为将要插入的行,或替换正在更新的行。这使得触发器函数可以修改即将插入或更新的行。
不打算产生上述任何一种行为的行级 BEFORE 触发器,必须小心地返回传入的同一行作为结果(也就是说,INSERT 和 UPDATE 触发器返回 NEW 行, DELETE 触发器返回 OLD 行)。
行级 INSTEAD OF 触发器应当要么返回 NULL,表示它没有修改视图底层基表中的任何数据;要么返回传入的视图行(INSERT 和 UPDATE 操作返回 NEW 行,DELETE 操作返回 OLD 行)。非空返回值用来表明触发器已经在视图中完成了必要的数据修改。这会使该命令所影响的行数计数增加。仅对于 INSERT 和 UPDATE 操作,触发器在返回 NEW 行之前还可以修改它。这会改变 INSERT RETURNING 或 UPDATE RETURNING 返回的数据;当视图显示的数据与提供给它的数据不完全相同时,这很有用。
对于在操作之后触发的行级触发器,其返回值会被忽略,因此它们可以返回 NULL。
生成列还有一些额外注意事项。 存储型生成列会在 BEFORE 触发器之后、AFTER 触发器之前计算。因此,可以在 AFTER 触发器中检查生成值。在 BEFORE 触发器中,OLD 行如预期那样包含旧的 生成值,但 NEW 行尚未包含新的生成值,因此不应访问。对于 C 语言接口,此时该列内容未定义;更高层编程语言应阻止在 BEFORE 触发器中访问 NEW 行中的存储型 生成列。在 BEFORE 触发器中对生成列值所做的修改会被忽略并随后被覆盖。虚拟生成列在触发器触发时永远不会被计算。在 C 语言接口中,它们在触发器函数中的内容是未定义的;更高层编程语言应阻止在触发器中访问虚拟生成列。
如果在同一关系上的同一事件为其定义了多个触发器,它们会按触发器名称的字母顺序触发。对于 BEFORE 和 INSTEAD OF 触发器,每个触发器返回的、可能已经被修改过的行都会成为下一个触发器的输入。如果任何一个 BEFORE 或 INSTEAD OF 触发器返回 NULL,则会放弃对该行执行该操作,并且后续触发器(针对该行)都不会再触发。
触发器定义还可以指定一个布尔 WHEN 条件,用来测试是否应当触发该触发器。对于行级触发器,WHEN 条件可以检查该行各列的旧值和/或新值。(语句级触发器也可以有 WHEN 条件,不过这个特性对它们用处不大。)在 BEFORE 触发器中, WHEN 条件就在函数即将执行或本会执行之前求值,因此使用 WHEN 与在触发器函数开头测试同样的条件并无实质差别。不过,在 AFTER 触发器中,WHEN 条件会在行更改发生后立即求值,并决定是否要在语句末尾将一个事件排入队列以触发该触发器。因此,当 AFTER 触发器的 WHEN 条件不返回真时,就不必排队该事件,也不必在语句末尾重新取出该行。如果触发器只需要针对少数几行触发,这会让修改大量行的语句显著加快。 INSTEAD OF 触发器不支持 WHEN 条件。
通常,行级 BEFORE 触发器用于检查或修改将要插入或更新的数据。例如,BEFORE 触发器可以用来把当前时间写入某个 timestamp 列,或者检查该行的两个元素是否一致。行级 AFTER 触发器最适合用于把更新传播到其他表,或对其他表执行一致性检查。之所以这样分工,是因为 AFTER 触发器可以确定自己看到的是该行的最终值,而 BEFORE 触发器不能;它之后可能还会有其他 BEFORE 触发器触发。如果没有特定理由把触发器做成 BEFORE 或 AFTER,那么 BEFORE 形式更高效,因为关于该操作的信息无需一直保存到语句末尾。
如果触发器函数执行 SQL 命令,那么这些命令可能会再次触发触发器。这就是所谓的级联触发器。对级联层数没有直接限制。级联还有可能导致同一个触发器被递归调用;例如,一个 INSERT 触发器可能执行一条向同一个表再插入一行的命令,从而导致该 INSERT 触发器再次触发。因此,在这种场景下,避免无限递归是触发器编写者自己的责任。
若外键约束指定了引用动作(即级联更新或级联删除),这些动作会通过在引用表上执行 普通 SQL UPDATE 或 DELETE 命令完成。 特别地,引用表上存在的触发器会因这些更改而触发。若此类触发器修改或阻止了这些 命令之一的效果,最终可能破坏引用完整性。避免这种情况是触发器编写者的责任。
在定义触发器时,可以为它指定参数。在触发器定义中包含参数,是为了让需求相似的不同触发器能够调用同一个函数。举例来说,可以有一个通用触发器函数,它接受两个列名作为参数,把当前用户写入其中一个列,把当前时间戳写入另一个列。只要编写得当,这个触发器函数就应独立于它所作用的具体表。因此,同一个函数可用于任何具有适当列的表上的 INSERT 事件,例如自动跟踪某个事务表中记录的创建。如果把它定义成 UPDATE 触发器,还可以用来跟踪最近更新事件。
每种支持触发器的编程语言都有自己的方法,使触发器输入数据可供触发器函数使用。这些输入数据包括触发器事件的类型(例如 INSERT 或 UPDATE),以及 CREATE TRIGGER 中列出的任何参数。对于行级触发器,输入数据还包括 INSERT 和 UPDATE 触发器的 NEW 行,以及/或者 UPDATE 和 DELETE 触发器的 OLD 行。
默认情况下,语句级触发器无法检查该语句修改的各个行。但是 AFTER STATEMENT 触发器可以请求创建 过渡表,从而使受影响的行集合可供该触发器使用。AFTER ROW 触发器也可以请求过渡表,这样它们既能看到表中的整体变化,也能看到当前使其触发的那一行中的变化。如何检查过渡表,同样取决于所使用的编程语言;不过典型做法是让过渡表表现得像只读临时表一样,可以在触发器函数内部发出的 SQL 命令中访问。
如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。