9.1第三十六章

From PostgreSQL wiki
Jump to navigationJump to search

第36章 触发器


这章概括了关于触发器编写的内容。触发器可以用大多数过程语言编写,包括PL/pgSQL (第39章), PL/Tcl (第40章), PL/Perl (第41章), 还有 PL/Python (第42章)。 阅读完本章以后,你可以参照你最喜欢的语言相关的章节,找出用该语言编写触发器的相关细节。 尽管大多数人感觉用过程语言编写触发器相对容易些,但是用C语言同样也可以写出触发器来。 目前还是不能用单纯的SQL语句编写触发器函数。

触发器功能概述

一个触发器是一种声明,告诉数据库应该在执行特定的操作的时候执行特定的函数。触发器可以作用于表和视图。

作用于表时,触发器可以定义在一个INSERT,UPDATE 或者 DELETE 命令之前或者之后执行,要么是每个记录行被修改,要么是每执行一次 SQL都会触发。UPDATE 触发器还可以设置为UPDATE语句中SET子句包含的特定列发生改动时触发。如果发生触发器事件,那么将在合适的时刻调用触发器函数以处理该事件。

作用于视图时,触发器不可以定义在 INSERT,UPDATE 或者 DELETE 操作上。而是定义在视图中的每一行需要修改时。触发器的任务是对下层的基础表做必要的修改并返回被修改的行到视图中。作用于视图的触发器还可以被定义为执行INSERT,UPDATE或DELETE操作之前或之后执行一条SQL语句。

触发器函数必须在创建触发器之前定义。触发器函数必须声明为一个没有参数并且返回trigger类型的函数。(触发器函数通过特殊的TriggerData结构接收其输入,而不是用普通函数参数那种形式。)

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

PostgreSQL 提供按行触发和按语句触发的触发器。在按行触发的触发器里,在每一行被影响时触发一次。相比之下,一个按语句触发的触发器是在每执行一次合适的语句执行一次的,而不管影响的行数。特别是,一个影响零行的语句将仍然导致任何适用的按语句触发的触发器的执行。这两种类型的触发器有时候分别叫做行级别的触发器和语句级别的触发器。作用于TRUNCATE的触发器只能是语句级别的。对于视图而言,在某个操作之前或之后触发的触发器只能定义为语句级别,但是除了INSERT,UPDATE 或 DELETE之外,只能定义为行级别。

触发器也可以根据触发时机分类,分别为 BEFORE 触发器、AFTER 触发器和 INSTEAD OF触发器。语句级别的 BEFORE 触发器通常在语句开始做任何事情之前触发,而语句级别的 AFTER 触发器在语句的最后触发。这种类型的触发器可以定义在表和视图上。行级别的 BEFORE 触发器在对特定行进行操作的时候马上触发,而行级别的 AFTER 触发器在语句结束的时候触发(但是在任何语句级别的 AFTER 触发器之前)。这类触发器只能定义在表上。行级别的 INSTEAD OF 触发器只能定义在视图上,并且一旦对视图数据操作时触发。

语句级别的触发器函数应该总是返回 NULL。如果必要,触发器函数可以给调用它的执行者返回表中的数据行(一个类型为 HeapTuple 的数值)。那些在操作之前触发的行级触发器有以下选择:

  • 它可以返回 NULL 以忽略对当前行的操作。这就指示执行器不要执行调用该触发器的行级别操作(对特定行的插入、修改或者删除)。
  • 仅对行级别 INSERT 和 UPDATE 触发器而言,其返回将要被插入和更新的行。这样就允许触发器函数修改被插入或者更新的行。

行级别 BEFORE 触发器并没有这些行为,它返回传进来行(也就是说,对 INSERT 和 UPDATE 触发器来说返回新行,对 DELETE 触发器来说返回被删除的行)。

行级别 INSTEAD OF 触发器要么返回 NULL 以表示其没修改某视图的底层表,要么返回传进来的视图行(INSERT 和 UPDATE操作的 NEW 行, DELETE操作的 OLD 行)。一个非空的返回值标志着触发器修改了视图的相应数据。这将导致该命令所影响数据行的计数增加。对于 INSERT 和 UPDATE 操作,触发器在返回前可能修改 NEW 行。这将改变被 INSERT RETURNING 或 UPDATE RETURNING 返回的数据,当视图没能展示其数据时非常有用。

对于在操作之后触发的行级别的触发器,其返回值会被忽略,因此他们可以返回NULL。

如果多于一个触发器为同样的事件定义在同样的关系上, 触发器将按照由名字的字母顺序排序的顺序触发。 如果是事件之前触发的触发器,每个触发器返回的可能已经被修改过的行成为下一个触发器的输入。 如果任何事件之前触发的触发器返回 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 触发器再次激发。 避免这样的无穷递归的问题是触发器程序员的责任。

在定义一个触发器的时候,我们可以声明一些参数。在触发器定义里面包含参数的目的是允许类似需求的不同触发器调用同一个函数。 比如,我们可能有一个通用的触发器函数,接受两个字段名字,把当前用户放在第一个,而当前时间戳在第二个。 只要我们写得恰当,那么这个触发器函数就可以和触发它的特定表无关。这样同一个函数就可以用于有着合适字段的任何表的 INSERT 事件,实现自动跟踪交易表中的记录创建之类的问题。如果定义成一个 UPDATE 触发器,我们还可以用它跟踪最后更新的事件。

每种支持触发器的编程语言都有自己的方法让触发器函数得到输入数据。这些输入数据包括触发器事件的类型(比如,INSERT 或者 UPDATE)以及所有在 CREATE TRIGGER 里面列出的参数。对于行级触发器,输入数据也包括 INSERT 和 UPDATE 触发器的 NEW 行,和/或 UPDATE 和 DELETE 触发器的 OLD 行。语句级别的触发器目前没有任何方法检查改语句所修改行。

数据更新的可见性

如果你在你的触发器函数里执行 SQL 命令,并且这些命令访问触发器所对应的表,那么你必须知道触发器的可视性规则,因为这些规则决定这些 SQL 命令是否能看到触发器对应数据的改变。简单说:

  • 语句级别的触发器遵循简单的可视性原则:在语句之前(before)触发的触发器看不到所有语句做的修改, 而所有修改都可以被语句之后(after)触发的触发器看到。
  • 导致触发器触发的数据改变(插入,更新,或者删除)通常是不能被一个before触发器里面执行的 SQL 命令看到的, 因为它还没有发生。
  • 不过,在 before 触发器里执行的 SQL 命令将会看到在同一个外层命令前面处理的行做的数据改变。 这一点需要我们仔细,因为这些改变时间的顺序通常是不可预期的;一个影响多行的 SQL 命令可能以任意顺序访问这些行。
  • 同样的,行级INSTEAD OF触发器可以看到之前同一个外层命令的INSTEAD OF触发器影响的数据。
  • 在一个 after 触发器被触发的时候,所有外层命令产生的数据改变都已经完成,可以被所执行的 SQL 命令看到。

如果你的触发器是使用标准过程语言编写的,上面的陈述只适用于函数被声明为VOLATILE. 如果函数被声明为STABLE或者IMMUTABLE,其将看到所有调用命令所有的改变。

有关数据可视性规则的更多信息可以在 Section 43.4 找到。 Section 36.4 里的例子包含这些规则的演示。

用C语言写触发器

本章描述触发器函数的低层细节。只有当你用 C 书写触发器函数的时候才需要这些信息。如果你用某种高级语言写触发器,那么系统就会为你处理这些细节。在大多数情况下,你在书写自己的 C 触发器之前应该考虑使用过程语言。每种过程语言的文档里面有关于如何用该语言书写触发器的解释。

触发器函数必须使用"版本 1(version 1)"的函数管理器接口。

当一个函数被触发器管理器调用时,它不会收到任何普通参数,而是收到一个指向TriggerData结构的"环境"指针。C函数可以执行一个宏来检查是否被触发器管理器调用的:

CALLED_AS_TRIGGER(fcinfo)

该宏展开就是:

((fcinfo)->context != NULL && IsA((fcinfo)->context, TriggerData))

如果此宏返回真(TRUE),则可以安全地把fcinfo->context转换成类型 TriggerData * 然后使用这个指向 TriggerData 的结构。 函数本身绝不能更改 TriggerData 结构或者它指向的任何数据。

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;

where the members are defined as follows:

type

总是 T_TriggerData。

tg_event

描述调用函数的事件。可以使用tg_event宏来检查:
TRIGGER_FIRED_BEFORE(tg_event)
如果触发器在操作前触发,返回真。
TRIGGER_FIRED_AFTER(tg_event)
如果触发器在操作后触发,返回真。
TRIGGER_FIRED_INSTEAD(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语句触发,返回真。
TRIGGER_FIRED_BY_TRUNCATE(tg_event)
如果触发器被TRUNCATE语句触发,返回真。

tg_relation

是一个指向描述被触发的关系的结构的指针。 请参考utils/rel.h获取关于此结构的详细信息。 最让人感兴趣的事情是 tg_relation->rd_att(关系元组的描述) 和tg_relation->rd_rel->relname(关系名。这个变量的类型不是 char*,而是NameData。 如果你需要一份名字的字符串拷贝,用 SPI_getrelname(tg_relation)获取)。

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;
    char        tgenabled;
    bool        tgisinternal;
    Oid         tgconstrrelid;
    Oid         tgconstrindid;
    Oid         tgconstraint;
    bool        tgdeferrable;
    bool        tginitdeferred;
    int16       tgnargs;
    int16       tgnattr;
    int16      *tgattr;
    char      **tgargs;
    char       *tgqual;
} 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"       /* this is what you need to work with SPI */
#include "commands/trigger.h"   /* ... and triggers */

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

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;

    /* make sure it's called as a trigger at all */
    if (!CALLED_AS_TRIGGER(fcinfo))
        elog(ERROR, "trigf: not called by trigger manager");

    /* tuple to return to executor */
    if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
        rettuple = trigdata->tg_newtuple;
    else
        rettuple = trigdata->tg_trigtuple;

    /* check for null values */
    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;

    /* connect to SPI manager */
    if ((ret = SPI_connect()) < 0)
        elog(ERROR, "trigf (fired %s): SPI_connect returned %d", when, ret);

    /* get number of rows in table */
    ret = SPI_exec("SELECT count(*) FROM ttest", 0);

    if (ret < 0)
        elog(ERROR, "trigf (fired %s): SPI_exec returned %d", when, ret);

    /* count(*) returns int8, so be careful to convert */
    i = DatumGetInt64(SPI_getbinval(SPI_tuptable->vals[0],
                                    SPI_tuptable->tupdesc,
                                    1,
                                    &isnull));

    elog (INFO, "trigf (fired %s): there are %d rows in ttest", when, i);

    SPI_finish();

    if (checknull)
    {
        SPI_getbinval(rettuple, tupdesc, 1, &isnull);
        if (isnull)
            rettuple = NULL;
    }

    return PointerGetDatum(rettuple);
}

编译完源码后(参看 Section 35.9.6),声明函数和触发器:

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();

现在你可以测试触发器的操作:

=> INSERT INTO ttest VALUES (NULL);
INFO:  trigf (fired before): there are 0 rows in ttest
INSERT 0 0

-- Insertion skipped and AFTER trigger is not fired

=> SELECT * FROM ttest;
 x
---
(0 rows)

=> 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
                                       ^^^^^^^^
                             remember what we said about visibility.
INSERT 167793 1
vac=> SELECT * FROM ttest;
 x
---
 1
(1 row)

=> 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
                                       ^^^^^^
                             remember what we said about visibility.
INSERT 167794 1
=> SELECT * FROM ttest;
 x
---
 1
 2
(2 rows)

=> UPDATE ttest SET x = NULL WHERE x = 2;
INFO:  trigf (fired before): there are 2 rows in ttest
UPDATE 0
=> 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)

=> DELETE FROM ttest;
INFO:  trigf (fired before): there are 2 rows in ttest
INFO:  trigf (fired before): there are 1 rows in ttest
INFO:  trigf (fired after ): there are 0 rows in ttest
INFO:  trigf (fired after ): there are 0 rows in ttest
                                       ^^^^^^
                             remember what we said about visibility.
DELETE 2
=> SELECT * FROM ttest;
 x
---
(0 rows)

在src/test/regress/regress.c和spi里还有更复杂的操作。