Postgresql泛型触发函数?

时间:2015-10-26 17:12:41

标签: postgresql triggers primary-key

我是Postgresql的新手,所以如果我的问题毫无意义,请放纵。

我试图找到一种方法将我的数据库结构迁移到Postgresql,特别是,我发现函数非常方便,并且想让我的众多触发器更容易编写。

在我的数据库中,我使用标准last_modifiedlast_modified_by字段来跟踪更改。我还使用带有增量序列的标准主键。

有内置语法将序列链接到主键ID,但是因为我必须为last_modified字段编写触发器,所以我想知道是否可以使用泛型函数一次更新所有内容。

实施例: 表格ANIMAL包含字段AMIMAL_ID(主键,序列为SEQ_ANIMAL),字段LAST_MODIFIEDLAST_MODIFIED_BY

同样,我的表格PLANT包含字段PLANT_ID(主键,序列为SEQ_PLANT),字段LAST_MODIFIEDLAST_MODIFIED_BY

我想在我需要创建的4个触发器中创建一个通用函数。我希望得到这样的东西:

插入功能之前:

CREATE FUNCTION TRIGGER_BI(p_pkField text, p_Sequence text) RETURNS TRIGGER AS $$
DECLARE
        curtime timestamp := now();
BEGIN
    NEW.LAST_UPDATED := curtime;
    NEW.LAST_UPDATED_BY := current_user;
    NEW.p_pkField := nextval(p_Sequence);
END;
$$
LANGUAGE 'plpgsql' IMMUTABLE
SECURITY DEFINER;

更新前功能:

CREATE FUNCTION TRIGGER_BU() RETURNS TRIGGER AS $$
DECLARE
        curtime timestamp := now();
BEGIN
    NEW.LAST_UPDATED := curtime;
    NEW.LAST_UPDATED_BY := current_user;
END;
$$
LANGUAGE 'plpgsql' IMMUTABLE
SECURITY DEFINER;

现在,表ANIMAL的触发器: 在插入之前:

CREATE TRIGGER ANIMAL
BEFORE INSERT
    FOR EACH ROW EXECUTE PROCEDURE TRIGGER_BI("ANIMAL_ID", "SEQ_ANIMAL");

更新前:

CREATE TRIGGER ANIMAL
    BEFORE UPDATE
        FOR EACH ROW EXECUTE PROCEDURE TRIGGER_BU();

PLANT的触发器: 在插入之前:

CREATE TRIGGER PLANT
BEFORE INSERT
    FOR EACH ROW EXECUTE PROCEDURE TRIGGER_BI("PLANT_ID", "SEQ_PLANT");

更新前:

CREATE TRIGGER PLANT
    BEFORE UPDATE
        FOR EACH ROW EXECUTE PROCEDURE TRIGGER_BU();

是否有可能以这种方式获得通用的东西?

是!正确的语法是什么? 奖励:事件可能只有一个函数来完成所有工作,默认的空参数如果为空则不会更新序列。

是,但等等!这种做法的缺点是什么? (性能,安全性,还有其他需要考虑的因素)?

不!所以我真的需要为每个触发器执行功能吗?

更新 我明确地创建序列,因为我可能希望在几个表之间有共享序列。我们的想法是将共享序列用作一个唯一的父表,其中有几个子表在其主键的主键上具有外键。不要对这种方法发表评论,但我的基本理解是访问序列的下一个值比管理外键更有效...

更新2: 我找到了一些非常有趣的东西,几乎让我在那里 - 只是我的setValue功能不起作用......

这里是通用触发器:

CREATE OR REPLACE FUNCTION TRIGGER_FUNC() RETURNS TRIGGER AS $$
DECLARE
    p_pkField  text;
    p_Sequence text;
    pkValue    int;
BEGIN
    EXECUTE format('SELECT ($1).%I::int', TG_ARGV[0]) USING NEW INTO pkValue;
    p_Sequence := quote_ident(TG_ARGV[1]);
    IF pkValue IS NULL THEN
        SELECT setfieldValue(pg_typeof(NEW), TG_ARGV[0], nextval(p_Sequence));
    END IF;
    NEW.LAST_UPDATED := curtime;
    NEW.LAST_UPDATED_BY := current_user;
    RETURN NEW;
END;
$$
LANGUAGE 'plpgsql' IMMUTABLE
SECURITY DEFINER;

我找到了setValue函数here解决方案的提示,并尝试对其进行调整,但它不起作用 - 我只是使用了错误的调用吗?或者我可以在方法中使用一些额外的知识来使其更简单吗? (我已经使用了我设置bigint值的事实,但我可能会做得更好?!)

这里是(非工作)代码:

CREATE OR REPLACE FUNCTION public.setfieldValue(anyelement, text, bigint)
 RETURNS anyelement
 LANGUAGE plpgsql
AS $function$
DECLARE 
  _name text;
  _values text[];
  _value text;
  _attnum int;
BEGIN
  FOR _name, _attnum
     IN SELECT a.attname, a.attnum
          FROM pg_catalog.pg_attribute a 
         WHERE a.attrelid = (SELECT typrelid
                               FROM pg_type
                              WHERE oid = pg_typeof($1)::oid) 
  LOOP
    IF _name = $2 THEN
      _value := $3;
    ELSE
      EXECUTE 'SELECT (($1).' || quote_ident(_name) || ')::text' INTO _value USING $1;
    END IF;
    _values[_attnum] :=  COALESCE('"' || replace(replace(_value, '"', '""'), '''', '''''') || '"', ''); 
  END LOOP;
  EXECUTE 'SELECT (' || quote_ident(pg_typeof($1)::text) || ' ''(' || array_to_string(_values,',') || ')'').*' INTO $1; 
  RETURN $1;
END;
$function$;

2 个答案:

答案 0 :(得分:3)

用于手动设置主键的默认值。 使用主键serial(或bigserial)声明您的表,并使用内置机制来处理此类列。 不要担心主键的值不连续。 主键仅用于明确标识行,而不是用于识别行。

除此之外,你不能这样做,因为触发器函数不能有声明的参数。

用于设置许多表中常用列的值。您可以使用相同的触发器功能进行插入和更新。例如:

CREATE OR REPLACE FUNCTION generic_trigger() -- function without arguments
RETURNS TRIGGER AS $$
BEGIN
    NEW.last_modified := now();
    NEW.last_modified_by := current_user;
    RETURN NEW;     -- important!
END;
$$
LANGUAGE 'plpgsql';

create table table_a 
    (a_id serial primary key, last_modified timestamp, last_modified_by text);
create table table_b 
    (b_id serial primary key, last_modified timestamp, last_modified_by text);

create trigger table_a_trigger
before insert or update on table_a
for each row execute procedure generic_trigger();

create trigger table_b_trigger
before insert or update on table_b
for each row execute procedure generic_trigger();

insert into table_a default values;

select * from table_a;

 a_id |      last_modified      | last_modified_by 
------+-------------------------+------------------
    1 | 2015-10-26 19:14:34.642 | postgres
(1 row) 

可能你有非常重要的理由在触发器中设置主键的值(请参阅jpmc26的注释)。 在这种情况下,主键应声明为integerbigint)而不是default expression,触发函数应如下所示:

create or replace function generic_trigger()
returns trigger as $$
begin
    new.last_modified := now();
    new.last_modified_by := current_user;
    if tg_op = 'INSERT' then
        if tg_table_name = 'table_a' then 
            new.a_id:= nextval('table_a_a_id_seq');
        elsif tg_table_name = 'table_b' then 
            new.b_id:= nextval('table_b_b_id_seq');
        end if;
    end if;
    return new;
end;
$$
language 'plpgsql';

insert into table_a (a_id) values (15);
select * from table_a;

 a_id |      last_modified      | last_modified_by 
------+-------------------------+------------------
    1 | 2015-10-26 19:14:34.642 | postgres
    2 | 2015-10-26 21:15:49.243 | postgres
(2 rows)

详细了解Trigger Procedures

答案 1 :(得分:2)

自动递增代理键

SERIAL会更好:

CREATE TABLE animal
(
    animal_id SERIAL PRIMARY KEY,
    ...
);

请注意,SERIAL实际上只是语法糖。它为您节省了一些麻烦:

  1. 创建序列
  2. 设置DEFAULT的{​​{1}}以获取下一个序列值。
  3. 创建"所有权"列与序列之间的关系。 (如果你丢弃表格,顺序就会随之而去。)
  4. 使列animal_id
  5. 但是使用NOT NULL时存在问题。如果某人SERIAL具有显式值,则将使用该值而不是从序列中生成一个值:

    INSERT

    如果您的应用程序连接到数据库而不是真实用户,则不必担心这一点;开发人员比明确指定他们想要生成的值更清楚。但是,如果您有真实用户直接登录到数据库,这可能是一个问题。您引用INSERT INTO animal (animal_id, ...) VALUES(50000, ...); 这一事实让我觉得这可能是您的用例。正如您所建议的那样,触发器将解决该问题:

    CURRENT_USER

    (请注意,如果他们明确尝试设置值,我会抛出错误而不是覆盖。对于您的用户来说,通常更快失败而不是默默地更改他们指定的值。)

    不幸的是,您确实需要每个表CREATE TABLE animal (animal_id INTEGER PRIMARY KEY, name TEXT NOT NULL); CREATE SEQUENCE animal_id_seq; ALTER SEQUENCE animal_id_seq OWNED BY animal.animal_id; CREATE OR REPLACE FUNCTION generate_animal_id() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN IF NEW.animal_id IS NOT NULL THEN RAISE EXCEPTION 'Cannot specify animal_id on INSERT: %', NEW.animal_id; ELSE NEW.animal_id = NEXTVAL('animal_id_seq'::regclass); RETURN NEW; END IF; END $$ ; CREATE TRIGGER animal_insert_generate_pk BEFORE INSERT ON animal FOR EACH ROW EXECUTE PROCEDURE generate_animal_id(); ,因为每个表都需要使用不同的序列。

    自动更新审核数据

    让我先谈谈FUNCTION。是的,你需要一个触发器。您可以为此处的所有表定义单个函数,但是:

    UPDATE

    离开CREATE OR REPLACE FUNCTION populate_last_updated_columns() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN IF NEW.last_updated IS NOT NULL OR NEW.last_updated_by IS NOT NULL THEN RAISE EXCEPTION 'Cannot specify last_updated or last_updated_by: %, %', NEW.last_updated, NEW.last_updated_by; END IF; NEW.last_updated = CURRENT_TIMESTAMP; NEW.last_updated_by = CURRENT_USER; RETURN NEW; END; $$ ; CREATE TRIGGER update_animal BEFORE UPDATE ON animal FOR EACH ROW EXECUTE PROCEDURE populate_last_updated_columns(); CREATE TRIGGER update_plant BEFORE UPDATE ON plant FOR EACH ROW EXECUTE PROCEDURE populate_last_updated_columns(); 。同样,这取决于您是否有真实用户连接到数据库并编写查询。如果,那么您应该利用INSERT来简化;再次,开发人员足够聪明,不能手动指定这些,只是让他们填写:

    DEFAULT

    但是如果您有用户直接连接并编写自己的查询,那么使用CREATE TABLE animal ( ... last_updated TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, last_updated_by TEXT NOT NULL DEFAULT CURRENT_USER, ... ); 来阻止他们手动指定这些值可能是有意义的。您可以重用与TRIGGER触发器相同的功能:

    UPDATE

    请注意,PostgreSQL允许您为同一事件定义多个触发器。 (它们在alphabetical order中执行。)在这种情况下,我宁愿继续将触发器分开。由于这两个触发器不可能影响给定行上的相同数据,因此这将更容易维护。它减少了代码量,这意味着对CREATE TRIGGER animal_insert_last_updated_cols BEFORE INSERT ON animal FOR EACH ROW EXECUTE PROCEDURE populate_last_updated_columns(); 函数的更改将以最小的麻烦更新所有触发器。我不会担心多个触发器的性能,除非你期望每秒几十populate_last_updated_columns秒。如果是,那么就进行基准测试,看看性能影响是否对您有影响。