PostgreSQL教程(四十):服务器编程(一)之 触发器

一、触发器行为概述

一个触发器声明了当执行一种特定类型的操作时数据库应该自动执行一个特殊的函数。触发器可以被附加到表(分区的或者不分区的)、视图和外部表。

在表和外部表上触发器可以被定义为在 INSERTUPDATE或 DELETE操作之前或之后被执行, 可以为每个SQL语句被执行一次或者为每个修改的行 被执行一次。UPDATE 触发器可以进一步地设置为只针对UPDATE 语句的SET子句的特定列出发。触发器也可以被 TRUNCATE语句触发。如果一个触发器事件发生, 触发器函数会在适当的事件被调用来处理该事件。

在视图上触发器可以被定义来取代INSERTUPDATE或 DELETE操作的执行。这种INSTEAD OF触发器对视图中需要被修改的每一行触发一次。触发器函数的职责是对视图的底层基本表执行必要的修改,并且在合适的时候返回被修改的行以便显示在视图中。视图上的触发器也可以被定义为对每个SQL语句执行一次,在INSERT\UPDATEDELETE操作之前或之后。不过,只有在该视图上还有一个INSTEAD OF触发器时,上述那些触发器才会被触发。否则,以该视图为目标的任何语句都必须被重写成一个影响其底层基表的语句,然后附着在那些基表上的触发器将会被引发。

触发器函数必须在触发器本身被创建之前被定义好。触发器函数必须被定义成一个没有参数的函数,并且返回类型为trigger(触发器函数通过一个特殊传递的TriggerData结构作为其输入,而不是以普通函数参数的形式)。

一旦一个合适的触发器函数被创建,就可以使用CREATE TRIGGER建立触发器。同一个触发器函数可以被用于多个触发器。

PostgreSQL同时提供每行的触发器和每语句的触发器。对于一个每行的触发器,对于触发触发器的语句所修改的每一行都会调用一次触发器函数。相反,一个每语句的触发器对于其触发语句只被调用一次,而不管该语句影响了多少行。特别地,一个不影响任何行的语句仍然会导致任何可用每语句的触发器的执行。这两类触发器有时也分别被称作行级触发器和语句级触发器。TRUNCATE上的触发器只能被定义在语句级。

触发器也可以根据它们是否在操作之前之后触发,或者被触发来取代操作来分类。它们分别指BEFORE触发器、AFTER 触发器以及INSTEAD OF触发器。语句级BEFORE触发器在语句开始做任何事情之前被触发,而语句级AFTER触发器则在语句做完所有事情之后被触发。这些触发器类型可以被定义在表、视图或外部表上。行级BEFORE触发器在每一个行被操作之前被触发,而行级AFTER触发器在语句结束之后被触发(但在任何语句级AFTER触发器之前)。这些触发器类型只能被定义在非分区表和外部表上,但不能定义在视图上。INSTEAD OF触发器只能被定义在视图上,并且只能定义在行级,当视图中的每一行被标识为需要被操作时,它们会立即触发。

一个以继承或者分区层次中父表为目标的语句不会导致受影响的子表的语句级触发器被引发,只有父表的语句级触发器会被引发。不过,受影响的子表的行级触发器将被引发。

如果一个INSERT包含ON CONFLICT DO UPDATE子句并且引用了EXCLUDED列,有可能所有行级 BEFORE INSERT触发器和所有行级 BEFORE UPDATE触发器的效果可能会以一种对于 被更新行最终状态透明的方式被应用。不过,对于要执行的两种集合的行级 BEFORE触发器都不需要有EXCLUDED列引用。当同时有行级 BEFORE INSERT和 BEFORE UPDATE触发器影响被插入/ 更新的行时(如果在两者不幂等时修改或多或少地等价,这仍可能是有问题的), 应该考虑可能出现的意料之外的结果。注意在指定了 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 DELETEAFTER INSERT触发器会被应用,但AFTER UPDATE触发器不会被应用,因为UPDATE已经被转换成了一个DELETE和一个INSERT。对于语句级触发器,即便发生行移动,DELETEINSERT触发器也都不会被引发,只有UPDATE语句中用到的目标表上的UPDATE触发器将被引发。

被语句级触发器调用的触发器函数应该总是返回NULL。根据行级触发器的选择,被其调用的触发器函数可以返回一个表行(类型HeapTuple的一个值)给执行器。在一个操作前触发的行级触发器有下列选择:

  • 它可以返回NULL来跳过对当前行的操作。这指示执行器不要执行调用触发器的行级操作(对一个特定表行的插入、修改或删除)。

  • 仅对行级INSERTUPDATE触发器来说,被返回的行称为将要被插入的行或者替代将被更新的行。这允许触发器函数修改将要被插入或更新的行。

一个无意导致任何这些行为的行级BEFORE触发器必须小心地它的结果,使之和被传入的行一样(即,INSERTUPDATE触发器的NEW行,DELETE触发器的OLD行)。

一个行级INSTEAD OF触发器可以返回NULL来指示它没有修改任何来自于视图底层基表的数据,也可以返回被传入的视图行(INSERTUPDATE操作的NEW行,或者DELETE操作的OLD行)。一个非空返回值被用于标志触发器在视图中执行了必须的数据修改。这将会导致被命令修改的行计数被增加。对于INSERTUPDATE操作,触发器可能会在返回NEW行之前对其进行修改。这将会改变INSERT RETURNINGUPDATE RETURNING返回的数据,并在视图无法正确地显示提供给它的相同数据时有用。

对于在一个操作之后触发的行级触发器,返回值会被忽略,因此它们可以返回NULL

如果为同一个关系上的同一事件定义了超过一个触发器,它们将按照其名称的字母表顺序被触发。在BEFOREINSTEAD OF触发器的情况下,每一个触发器返回的可能被修改的行将成为下一个触发器的输入。如果任何一个BEFOREINSTEAD OF触发器返回NULL,该操作将在该行上被禁用并且对于该行不会触发后续的触发器。

一个触发器定义也能指定一个布尔的WHEN条件,它将被测试来看该触发器是否应该被触发。在行级触发器中,WHEN条件可以检查该行的旧列值和/或新列值(语句级触发器也能有WHEN条件,但是该特性对它们不太有用)。在一个BEFORE触发器中,WHEN条件只是在该函数被或者将被执行前计算,因此使用WHEN条件与在该触发器函数的开始测试相同的条件没有本质区别。不过,在一个AFTER触发器中,WHEN条件只是在行更新发生之后被计算,并且它决定在语句的末尾一个事件是否被排队来触发该触发器。因此当一个AFTER触发器的WHEN不返回真时,在语句的末尾没有必要将一个事件进行排队,也没有必要重新取出该行。如果触发器只对少数行触发,这可以使得修改很多行的语句明显加快。INSTEAD OF触发器不支持WHEN条件。

通常,行级BEFORE被用来检查或修改即将被插入或更新的数据。例如,一个BEFORE触发器可以被用来把当前时间插入到一个timestamp列中,或者检查该行的两个元素之间是否一致。行级AFTER触发器大多数被用来将更新传播到其他表,或者针对其他表进行一致性检查。进行这种工作分工的原因是,一个AFTER触发器可以肯定它看到的是该行的最终值,而一个BEFORE触发器则不能,因为还可能有其他BEFORE触发器在它之后触发。如果你不知道让一个触发器是BEFOREAFTER,则BEFORE形式更加有效,因为关于该操作的信息直到语句的末尾都不需要被保存。

如果一个触发器函数执行 SQL 命令,则这些命令可能会再次引发触发器。这就是所谓的级联触发器。对于级联的层数没有直接的限制。级联有可能会导致对同一个触发器的递归调用。例如,一个INSERT触发器可能执行一个向同一个表插入一个额外行的命令,这就导致该INSERT触发器被再次引发。所以在这种情形下,触发器程序员应该负责避免无限递归。

在定义一个触发器时,可以为它指定参数。在触发器定义中包括参数的目的是允许具有相似需求的不同触发器调用同一个函数。例如,可能有一个一般性的触发器函数,它需要两个列名作为参数,一个放当前用户而另一个放当前时间戳。在正确编写的情况下,这个触发器函数应该独立于它所触发的表。因此同一个函数可以被用于具有适当列的任意表上的INSERT事件,这样做的用途之一是可以自动追踪一个交易表中记录的创建。如果被定义成一个UPDATE触发器,它也可以被用来追踪最新的更新事件。

每一种支持触发器的编程语言都有自己的方法来让触发器输入数据对触发器函数可用。这种输入数据包括触发器事件的类型(如INSERTUPDATE)以及被列在CREATE TRIGGER中的任何参数。对于一个行级触发器,输入数据还包括用于INSERTUPDATE触发器的NEW行,和/或用于UPDATEDELETE触发器的OLD行。语句级触发器当前没有任何方法检查被语句修改的单个行。

默认情况下,语句级触发器没有办法检查该语句修改的行。但是AFTER STATEMENT触发器可以请求创建传递表,这样可以让受影响的行集合对该触发器可用。AFTER ROW触发器也可以请求传递表,这样它们可以看到表中的整个变化,同时也能看到当前引发它们的个体行中的变化。检查传递表的方法仍是取决于使用的编程语言,但是通常的方法让传递表变得像触发器函数内部发出的SQL命令能够访问的只读临时表一样。

二、数据改变的可见性

如果你在你的触发器函数中执行 SQL 命令,并且这些命令会访问触发器所在的表,那么你需要注意数据可见性规则。因为这些规则决定了这些 SQL 命令是否将能看见引发触发器的数据改变。简单地:

  • 语句级触发器遵循简单的可见性规则:一个语句所作的改变对于语句级 BEFORE触发器都不可见,而所有修改对于语句级 AFTER触发器都是可见的。

  • 导致触发器被引发的数据更改(插入、更新或删除)自然对于在一个行级BEFORE触发器中执行的 SQL 命令可见,因为它还没有发生。

  • 但是,在一个行级BEFORE触发器中执行的 SQL 命令将会看见之前在同一个外层命令中所作的数据更改的效果。这里需要小心,因为这些更改时间的顺序通常是不可预测的,一个影响多行的 SQL 命令可能以任何顺序访问这些行。

  • 类似地,一个行级INSTEAD OF触发器将会看见之前在同一个外层命令中INSTEAD OF触发器引发所作的数据更改。

  • 当一个行级AFTER触发器被引发时,所有由外层命令所作的数据更改已经完成,并且对于该被调用的触发器函数是可见的。

如果你的触发器函数使用任何一种标准过程语言编写的,那么只有在该函数被声明为VOLATILE时上述陈述才适用。被声明为STABLEIMMUTABLE的函数在任何情况下将不能看到由调用命令所作出的更改。

三、事件触发器

为了对上述触发器中讨论的触发器机制加以补充, PostgreSQL也提供了事件触发器。和常规触发器(附着在 一个表上并且只捕捉 DML 事件)不同,事件触发器对一个特定数据库来说是全局 的,并且可以捕捉 DDL 事件。

和常规触发器相似,可以用任何包括了事件触发器支持的过程语言或者 C 编写 事件触发器,但是不能用纯 SQL 编写。

四、事件触发器行为总览

只要与一个事件触发器相关的事件在事件触发器所在的数据库中发生, 该事件触发器就会被引发。当前支持的事件是 ddl_command_startddl_command_end、 table_rewritesql_drop。未来的发行版 中可能会增加对更多事件的支持。

ddl_command_start事件就在CREATE、 ALTERDROPSECURITY LABEL、 COMMENTGRANT或者REVOKE 命令的执行之前发生。在事件触发器引发前不会做受影响对象是否存在的检查。 不过,一个例外是,这个事件不会为目标是共享对象 — 数据库、角色 以及表空间 — 的 DDL 命令发生,也不会为目标是事件触发器的 DDL 命令发生。事件触发器机制不支持这些对象类型。 ddl_command_start也会在SELECT INTO 命令的执行之前发生,因为这等价于 CREATE TABLE AS

ddl_command_end事件就在同一组命令的执行之后发生。为了 得到发生的DDL操作的更多细节,可以从 ddl_command_end事件触发器代码中使用集合返回函数 pg_event_trigger_ddl_commands()。注意该触发器是在那些动作 已经发生之后(但是在事务提交前)引发,并且因此系统目录会被读作已更改。

sql_drop事件为任何删除数据库对象的操作在 ddl_command_end事件触发器之前发生。要列出已经被删除的 对象,可以从sql_drop事件触发器代码中使用集合返回函数 pg_event_trigger_dropped_objects()。注意该触发器是在对象已经 从系统目录删除以后执行,因此不能再查看它们。

table_rewrite事件在表被命令ALTER TABLE和 ALTER TYPE的某些动作重写之前发生。虽然其他控制语句(例如 CLUSTERVACUUM)也可以用来重 写表,但是它们不会触发table_rewrite事件。

不能在一个中止的事务中执行事件触发器(其他函数也一样)。因此,如果一个 DDL 命令出现错误失败,将不会执行任何相关的 ddl_command_end触发器。反过来,如果一个 ddl_command_start触发器出现错误失败,将不会引发进一步的 事件触发器,并且不会尝试执行该命令本身。类似地,如果一个 ddl_command_end触发器出现错误失败,DDL 命令的效果将被 回滚,就像其他包含事务中止的情况中那样。

事件触发器通过命令CREATE EVENT TRIGGER创建。为了 创建一个事件触发器,你必须首先创建一个有特殊返回类型 event_trigger的函数。这个函数不一定需要返回一个值, 该返回类型仅仅是作为一种信号表示该函数要被作为一个事件触发器调用。

如果对于一个特定的事件定义了多于一个事件触发器,它们将按照触发器名称 的字母表顺序被引发。

一个触发器定义也可以指定一个WHEN条件,这样事件触 发器(例如ddl_command_start触发器)就可以只对用户 希望介入的特定命令触发。这类触发器的通常用法是用于限制用户可能执行的 DDL 操作的范围。

五、事件触发器触发矩阵

表5.1列出了所有命令的 事件触发器支持情况。

表 5.1. 支持事件触发器的命令标签

命令标签ddl_command_startddl_command_endsql_droptable_rewrite注解
ALTER AGGREGATEXX--
ALTER COLLATIONXX--
ALTER CONVERSIONXX--
ALTER DOMAINXX--
ALTER EXTENSIONXX--
ALTER FOREIGN DATA WRAPPERXX--
ALTER FOREIGN TABLEXXX-
ALTER FUNCTIONXX--
ALTER LANGUAGEXX--
ALTER OPERATORXX--
ALTER OPERATOR CLASSXX--
ALTER OPERATOR FAMILYXX--
ALTER POLICYXX--
ALTER SCHEMAXX--
ALTER SEQUENCEXX--
ALTER SERVERXX--
ALTER TABLEXXXX
ALTER TEXT SEARCH CONFIGURATIONXX--
ALTER TEXT SEARCH DICTIONARYXX--
ALTER TEXT SEARCH PARSERXX--
ALTER TEXT SEARCH TEMPLATEXX--
ALTER TRIGGERXX--
ALTER TYPEXX-X
ALTER USER MAPPINGXX--
ALTER VIEWXX--
CREATE AGGREGATEXX--
COMMENTXX--只对本地对象
CREATE CASTXX--
CREATE COLLATIONXX--
CREATE CONVERSIONXX--
CREATE DOMAINXX--
CREATE EXTENSIONXX--
CREATE FOREIGN DATA WRAPPERXX--
CREATE FOREIGN TABLEXX--
CREATE FUNCTIONXX--
CREATE INDEXXX--
CREATE LANGUAGEXX--
CREATE OPERATORXX--
CREATE OPERATOR CLASSXX--
CREATE OPERATOR FAMILYXX--
CREATE POLICYXX--
CREATE RULEXX--
CREATE SCHEMAXX--
CREATE SEQUENCEXX--
CREATE SERVERXX--
CREATE STATISTICSXX--
CREATE TABLEXX--
CREATE TABLE ASXX--
CREATE TEXT SEARCH CONFIGURATIONXX--
CREATE TEXT SEARCH DICTIONARYXX--
CREATE TEXT SEARCH PARSERXX--
CREATE TEXT SEARCH TEMPLATEXX--
CREATE TRIGGERXX--
CREATE TYPEXX--
CREATE USER MAPPINGXX--
CREATE VIEWXX--
DROP AGGREGATEXXX-
DROP CASTXXX-
DROP COLLATIONXXX-
DROP CONVERSIONXXX-
DROP DOMAINXXX-
DROP EXTENSIONXXX-
DROP FOREIGN DATA WRAPPERXXX-
DROP FOREIGN TABLEXXX-
DROP FUNCTIONXXX-
DROP INDEXXXX-
DROP LANGUAGEXXX-
DROP OPERATORXXX-
DROP OPERATOR CLASSXXX-
DROP OPERATOR FAMILYXXX-
DROP OWNEDXXX-
DROP POLICYXXX-
DROP RULEXXX-
DROP SCHEMAXXX-
DROP SEQUENCEXXX-
DROP SERVERXXX-
DROP STATISTICSXXX-
DROP TABLEXXX-
DROP TEXT SEARCH CONFIGURATIONXXX-
DROP TEXT SEARCH DICTIONARYXXX-
DROP TEXT SEARCH PARSERXXX-
DROP TEXT SEARCH TEMPLATEXXX-
DROP TRIGGERXXX-
DROP TYPEXXX-
DROP USER MAPPINGXXX-
DROP VIEWXXX-
GRANTXX--只对本地对象
IMPORT FOREIGN SCHEMAXX--
REVOKEXX--只对本地对象
SECURITY LABELXX--只对本地对象
SELECT INTOXX--
  • 23
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值