【问题标题】:Postgresql generic trigger function?Postgresql 通用触发函数?
【发布时间】:2015-10-26 17:12:40
【问题描述】:

我是 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$;

【问题讨论】:

  • 触发器对我来说看起来不错。当你尝试它们时发生了什么?
  • 您是否有多个用户使用不同的数据库凭据连接?还是CURRENT_USER 只是为了演示? (也就是说,你有真正的用户连接到数据库还是只有应用程序?)
  • 考虑只有应用程序正在连接,而 CURRENT_USER 实际上是使用该应用程序的用户(我仍然不知道我是否想为每个应用程序用户创建一个 DB 用户并处理 DB 用户的登录和凭据,或者如果我将以某种方式将用户名传递给查询)。
  • 我不是正统的,我真的很喜欢编程的新鲜视角。但是,您的想法似乎并不恰当。外键主要用于控制参照完整性。内置机制以最好的方式实现它,而在你的方法中我看不到这种控制。很难想出比多年来开发并经过数百万次测试的解决方案更好的解决方案,尤其是在主键和外键等基本功能方面。

标签: postgresql triggers primary-key


【解决方案1】:

No 用于手动设置主键的默认值。 使用主键将您的表声明为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 的评论)。 在这种情况下,主键应声明为 integer (bigint) 而没有 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的信息。

【讨论】:

  • 触发器未尝试分配“无缝”值。它使用序列,就像serial 列将
  • 有一个可能的用例:如果有人手动指定了主键值,那么由SERIAL 生成的DEFAULT 子句将被忽略。当序列生成已使用的值时,另一个用户稍后会收到一个神秘的错误。如果有人尝试使用显式代理键值INSERT,OP 可能希望显式覆盖该值或引发错误。我认为这需要一个触发器。这就是为什么我询问 OP 是否有用户直接与数据库交互;这是我认为在SERIAL 上使用触发器有意义的唯一情况。
  • 很好的答案。请注意,序列在许多情况下很方便,但在我的设计中(不管它是否坏,但它是如何完成的),我有共享序列的表 - 这比使用具有两个外键的公共父表要容易得多,这是最接近我需要的。使用上述方法轻松:我可以决定每个表的表,这是触发器调用方法的顺序。我相应地更新了我的问题。
【解决方案2】:

自动递增代理键

SERIAL 会更好:

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

请注意,SERIAL 实际上只是语法糖。它为您做了一些事情来省去您的麻烦:

  1. 创建一个序列
  2. 设置animal_idDEFAULT 以获取下一个序列值。
  3. 在列和序列之间创建“所有权”关系。 (如果你放下桌子,顺序也会随之而来。)
  4. 使列NOT NULL

不过,使用SERIAL 存在问题。如果某人 INSERTs 具有显式值,则将使用该值而不是从序列中生成一个值:

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 触发器相同的功能:

CREATE TRIGGER animal_insert_last_updated_cols
BEFORE INSERT ON animal
FOR EACH ROW
EXECUTE PROCEDURE populate_last_updated_columns();

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

【讨论】:

  • 实际上...我可能有一个想法来概括该代理触发器。我得考虑一下。
  • 我做了额外的研究,并提出了一个相当接近我想要的解决方案 - 但它还没有工作。也许更新 2 会给出提示?!
猜你喜欢
  • 2015-07-18
  • 2017-08-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-05
  • 2020-05-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多