触发器函数必须在创建触发器之前,作为一个没有参数并且返回trigger类型的函数定义。 (触发器函数通过特殊的 TriggerData 结构接收其输入,而不是用普通函数参数那种形式。)
一旦创建了一个合适的触发器函数,触发器就用 CREATE TRIGGER 创建。同一个触发器函数可以用于多个触发器。
有两种类型的触发器:按行触发的触发器和按语句触发的触发器。在按行触发的触发器里, 触发器函数是为触发触发器的语句影响的每一行执行一次。相比之下,一个按语句触发的触发器是在每执行一次合适的语句执行一次的, 而不管影响的行数。特别是,一个影响零行的语句将仍然导致任何适用的按语句触发的触发器的执行。 这两种类型的触发器有时候分别叫做"行级别的触发器"和"语句级别的触发器"。
语句级别的 "before" 触发器通常在语句开始做任何事情之前触发, 而语句级别的 "after" 触发器在语句的最后触发。 行级别的 "before" 触发器在对特定行进行操作的时候马上触发, 而行级别的 "after" 触发器在语句结束的时候触发(但是在任何语句级别的 "after" 触发器之前)。
按语句触发的触发器应该总是返回 NULL。 如果必要,按行触发的触发器函数可以给调用它的执行者返回一表数据行(一个类型为 HeapTuple 的数值), 那些在操作之前触发的触发器有以下选择:
它可以返回 NULL 以忽略对当前行的操作。 这就指示执行器不要执行调用该触发器的行级别操作(对特定行的插入或者更改))。
只用于INSERT和UPDATE触发器: 返回的行将成为被插入的行或者是成为将要更新的行。 这样就允许触发器函数修改被插入或者更新的行。
一个无意导致任何这类行为的在操作之前触发的行级触发器必须仔细返回那个被当作新行传进来的同一行 (也就是说,对于 INSERT 和 UPDATE 触发器而言,是 NEW 行, 对于 DELETE 触发器而言,是 OLD 行)。
对于在操作之后触发的行级别的触发器,其返回值会被忽略,因此他们可以返回NULL。
如果多于一个触发器为同样的事件定义在同样的关系上, 触发器将按照由名字的字母顺序排序的顺序触发。 如果是事件之前触发的触发器,每个触发器返回的可能已经被修改过的行成为下一个触发器的输入。 如果任何事件之前触发的触发器返回 NULL 指针, 那么其操作被丢弃并且随后的触发器不会被触发。
通常,行的 before 触发器用于检查或修改将要插入或者更新的数据。 比如,一个 before 触发器可以用于把当前时间插入一个时间戳字段, 或者跟踪该行的两个元素是一致的。行的 after 触发器多数用于填充或者更新其它表, 或者对其它表进行一致性检查。这么区分工作的原因是, after 触发器肯定可以看到该行的最后数值, 而 before 触发器不能;还可能有其它的 before 触发器在其后触发。 如果你没有具体的原因定义触发器是 before 还是 after,那么 before 触发器的效率高些, 因为操作相关的信息不必保存到语句的结尾。
如果一个触发器函数执行 SQL 命令,然后这些命令可能再次触发触发器。 这就是所谓的级联触发器。对级联触发器的级联深度没有明确的限制。 有可能出现级联触发器导致同一个触发器的递归调用的情况; 比如,一个 INSERT 触发器可能执行一个命令, 把一个额外的行插入同一个表中,导致 INSERT 触发器再次激发。 避免这样的无穷递归的问题是触发器程序员的责任。
在定义一个触发器的时候,我们可以声明一些参数。 在触发器定义里面包含参数的目的是允许类似需求的不同触发器调用同一个函数。 比如,我们可能有一个通用的触发器函数, 接受两个字段名字,把当前用户放在第一个,而当前时间戳在第二个。 只要我们写得恰当,那么这个触发器函数就可以和触发它的特定表无关。 这样同一个函数就可以用于有着合适字段的任何表的 INSERT 事件,实现自动跟踪交易表中的记录创建之类的问题。如果定义成一个 UPDATE 触发器,我们还可以用它跟踪最后更新的事件。
每种支持触发器的编程语言都有自己的方法让触发器函数得到输入数据。 这些输入数据包括触发器事件的类型(比如,INSERT 或者 UPDATE)以及所有在 CREATE TRIGGER 里面列出的参数。 对于低层次的触发器,输入数据也包括 INSERT 和 UPDATE 触发器的 NEW 行,和/或 UPDATE 和 DELETE 触发器的 OLD 行。 语句级别的触发器目前没有任何方法检查改语句修改的独立行。
如果你在你的触发器函数里执行 SQL 命令,并且这些命令访问触发器所在的表, 那么你必须知道触发器的可视性规则,因为这些规则决定这些 SQL 命令是否能看到触发触发器的数据改变。 简单说:
语句级别的触发器遵循简单的可视性原则:在语句之前(before)触发的触发器看不到所有语句做的修改, 而所有修改都可以被语句之后(after)触发的触发器看到。
导致触发器触发的数据改变(插入,更新,或者删除)通常是不能被一个before触发器里面执行的 SQL 命令看到的, 因为它还没有发生。
不过,在 before 触发器里执行的 SQL 命令将会看到在同一个外层命令前面处理的行做的数据改变。 这一点需要我们仔细,因为这些改变时间的顺序通常是不可预期的; 一个影响多行的 SQL 命令可能以任意顺序访问这些行。
在一个 after 触发器被触发的时候,所有外层命令产生的数据改变都已经完成, 可以被所执行的 SQL 命令看到
本章描述触发器函数的低层细节。只有当你用C书写触发器函数的时候才需要这些信息。 如果你用某种高级语言写触发器,那么系统就会为你处理这些细节。 每种过程语言的文档里面有关于如何用该语言书写触发器的解释。
触发器函数必须使用"版本 1(version 1)"的函数管理器接口。
当一个函数被触发器管理器调用时,它不会收到任何普通参数,而是收到一个指向TriggerData结构的"环境"指针。 C 函数可以通过执行实际上被扩展为
(fcinfo)->context != NULL && IsA((fcinfo)->context, TriggerData))
的宏
CALLED_AS_TRIGGER(fcinfo)
CALLED_AS_TRIGGER(fcinfo), 来判断自己是否从触发器管理器中调用的。 如果此宏返回真(TRUE),则可以安全地把fcinfo->context转换成类型 TriggerData * 然后使用这个指向 TriggerData 的结构。 函数本身绝不能更改 TriggerData 结构或者它指向的任何数据。
struct TriggerData 是在 commands/trigger.h 里面定义的:
typedef struct TriggerData
{
NodeTag type;
TriggerEvent tg_event;
Relation tg_relation;
HeapTuple tg_trigtuple;
HeapTuple tg_newtuple;
Trigger *tg_trigger;
Buffer tg_trigtuplebuf;
Buffer tg_newtuplebuf;
} TriggerData;
这些成员的定义如下:
type
总是 T_TriggerData。
tg_event
描述调用函数的事件。你可以用下面的宏检查 tg_event:
TRIGGER_FIRED_BEFORE(tg_event)
如果触发器是在操作前触发,返回真。
TRIGGER_FIRED_AFTER(tg_event)
如果触发器是在操作后触发,返回真。
TRIGGER_FIRED_FOR_ROW(tg_event)
如果触发器是行级别事件触发,返回真。
TRIGGER_FIRED_FOR_STATEMENT(tg_event)
如果触发器是语句级别事件触发,返回真。
TRIGGER_FIRED_BY_INSERT(tg_event)
如果触发器是由INSERT触发,返回真。
TRIGGER_FIRED_BY_UPDATE(tg_event)
如果触发器是由UPDATE触发,返回真。
TRIGGER_FIRED_BY_DELETE(tg_event)
如果触发器是由DELETE触发,返回真。
tg_relation
是一个指向描述被触发的关系的结构的指针。 请参考utils/rel.h获取关于此结构的详细信息。 最让人感兴趣的事情是 tg_relation->rd_att(关系行的描述) 和tg_relation->rd_rel->relname(关系名。这个变量的类型不是 char*,而是NameData。 如果你需要一份名字的拷贝,用 SPI_getrelname(tg_relation)获取char* )。
tg_trigtuple
是一个指向触发触发器的行的指针。这是一个正在被插入(INSERT), 删除(DELETE)或更新(UPDATE)的行。如果是 INSERT或DELETE, 那么这就是你将返回给执行者的东西 — 如果你不想用另一条行覆盖此行(INSERT)或忽略操作(在DELETE的时候)。
tg_newtuple
如果是UPDATE,这是一个指向新版本的行的指针,如果是INSERT 或DELETE, 就是NULL。这就是你将返回给执行者的东西 — 如果事件是 UPDATE 并且你不想用另一条行替换这条行或忽略操作。
tg_trigger
是一个指向结构Trigger的指针,该结构在utils/rel.h里定义:
typedef struct Trigger
{
Oid tgoid;
char *tgname;
Oid tgfoid;
int16 tgtype;
bool tgenabled;
bool tgisconstraint;
Oid tgconstrrelid;
bool tgdeferrable;
bool tginitdeferred;
int16 tgnargs;
int16 tgattr[FUNC_MAX_ARGS];
char **tgargs;
}
Trigger;
tgname是触发器的名称,tgnargs 是在tgargs里参数的数量, tgargs是一个指针数组,数组里每个指针指向在 CREATE TRIGGER语句里声明的参数。其他成员只在内部使用。
tg_trigtuplebuf
如果没有这样的远祖或者没有存储在磁盘缓冲区里, 则是包含 tg_trigtuple 或者 InvalidBuffer 的缓冲区。
tg_newtuplebuf
如果没有这样的远祖或者它并未存储在磁盘缓冲区里,那么就是包含 tg_newtuple, 或者 InvalidBuffer 的缓冲区。
一个触发器函数必须返回一个 HeapTuple 指针或者一个 NULL 指针 (不是 SQL NULL,也就是说,不要设置 isNull 为真)。 请注意如果你不想修改正在被操作的行,那么要根据情况返回 tg_trigtuple 或者 tg_newtuple。
这里是一个用 C 写的非常简单的触发器使用的例子。 函数trigf报告表 ttest 中行数量, 并且如果命令试图把空值插入到字段 x 里(也就是说 -它做为一个非空约束但不退出事务的约束)时略过操作。
首先,表定义:
CREATE TABLE ttest (
x integer
);
这里是触发器函数的源代码:
#include "postgres.h"
#include "executor/spi.h" /* 你用SPI的时候要用的头文件 */
#include "commands/trigger.h" /* 用触发器时要用的头文件 */
extern Datum trigf(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(trigf);
Datum
trigf(PG_FUNCTION_ARGS)
{
TriggerData *trigdata = (TriggerData *) fcinfo->context;
TupleDesc tupdesc;
HeapTuple rettuple;
char *when;
bool checknull = false;
bool isnull;
int ret, i;
/* 确信自己是作为触发器触发的 */
if (!CALLED_AS_TRIGGER(fcinfo))
elog(ERROR, "trigf: not fired by trigger manager");
/* 返回给执行者的行 */
if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
rettuple = trigdata->tg_newtuple;
else
rettuple = trigdata->tg_trigtuple;
/* 检查空值 */
if (!TRIGGER_FIRED_BY_DELETE(trigdata->tg_event)
&& TRIGGER_FIRED_BEFORE(trigdata->tg_event))
checknull = true;
if (TRIGGER_FIRED_BEFORE(trigdata->tg_event))
when = "before";
else
when = "after ";
tupdesc = trigdata->tg_relation->rd_att;
/* 与 SPI 管理器连接 */
if ((ret = SPI_connect()) < 0)
elog(INFO, "trigf (fired %s): SPI_connect returned %d", when, ret);
/* 获取关系中的行数量 */
ret = SPI_exec("SELECT count(*) FROM ttest", 0);
if (ret < 0)
elog(NOTICE, "trigf (fired %s): SPI_exec returned %d", when, ret);
/* count(*) 返回 int8,所以要小心转换 */
i = (int) DatumGetInt64(SPI_getbinval(SPI_tuptable->vals[0],
SPI_tuptable->tupdesc,
1,
&isnull));
elog (NOTICE, "trigf (fired %s): there are %d tuples in ttest", when, i);
SPI_finish();
if (checknull)
{
(void) SPI_getbinval(rettuple, tupdesc, 1, &isnull);
if (isnull)
rettuple = NULL;
}
return PointerGetDatum(rettuple);
}
编译完源代码后,声明函数并创建触发器:
CREATE FUNCTION trigf() RETURNS trigger
AS 'filename'
LANGUAGE C;
CREATE TRIGGER tbefore BEFORE INSERT OR UPDATE OR DELETE ON ttest
FOR EACH ROW EXECUTE PROCEDURE trigf();
CREATE TRIGGER tafter AFTER INSERT OR UPDATE OR DELETE ON ttest
FOR EACH ROW EXECUTE PROCEDURE trigf();
现在你可以测试触发器的操作:
vac=> INSERT INTO ttest VALUES (NULL);
INFO:trigf (fired before): there are 0 tuples in ttest
INSERT 0 0
-- 插入被忽略,AFTER 触发器没有触发
vac=> SELECT * FROM ttest;
x
---
(0 rows)
vac=> INSERT INTO ttest VALUES (1);
INFO:trigf (fired before): there are 0 rows in ttest
INFO:trigf (fired after ): there are 1 rows in ttest
^^^^^^^^
回忆一下我们讲的有关可视性的东西。
INSERT 167793 1
vac=> SELECT * FROM ttest;
x
---
1
(1 row)
vac=> INSERT INTO ttest SELECT x * 2 FROM ttest;
INFO: trigf (fired before): there are 1 rows in ttest
INFO: trigf (fired after ): there are 2 rows in ttest
^^^^^^^^
还记得我们讲过的关于可视性的原则吗
INSERT 167794 1
vac=> SELECT * FROM ttest;
x
---
1
2
(2 rows)
vac=> UPDATE ttest SET x = NULL where x = 2;
INFO: trigf (fired before): there are 2 rows in ttest
UPDATE 0
vac=> UPDATE ttest SET x = 4 where x = 2;
INFO: trigf (fired before): there are 2 rows in ttest
INFO: trigf (fired after ): there are 2 rows in ttest
UPDATE 1
vac=> SELECT * FROM ttest;
x
---
1
4
(2 rows)
vac=> DELETE FROM ttest;
INFO: trigf (fired before): there are 2 rows in ttest
INFO: trigf (fired after ): there are 1 rows in ttest
INFO: trigf (fired before): there are 1 rows in ttest
INFO: trigf (fired after ): there are 0 rows in ttest
^^^^^^^^
还记得我们讲过的关于可视性的原则吗
DELETE 2
vac=> SELECT * FROM ttest;
x
---
(0 rows)
在 src/test/regress/regress.c 和 contrib/spi 里还有更复杂的例子