更新plpgsql中触发器函数中的多个列

ssc*_*ber 3 sql postgresql triggers dynamic-sql plpgsql

给定以下架构:

create table account_type_a (
  id SERIAL UNIQUE PRIMARY KEY,
  some_column VARCHAR
);

create table account_type_b (
  id SERIAL UNIQUE PRIMARY KEY,
  some_other_column VARCHAR
);

create view account_type_a view AS select * from account_type_a;
create view account_type_b view AS select * from account_type_b;
Run Code Online (Sandbox Code Playgroud)

我尝试在plpgsql中创建一个通用的触发器函数,它可以更新视图:

create trigger trUpdate instead of UPDATE on account_view_type_a
for each row execute procedure updateAccount();    
create trigger trUpdate instead of UPDATE on account_view_type_a
for each row execute procedure updateAccount();
Run Code Online (Sandbox Code Playgroud)

我的不成功的努力是:

create function updateAccount() returns trigger as $$
declare
  target_table varchar := substring(TG_TABLE_NAME from '(.+)_view');
  cols varchar;
begin
  execute 'select string_agg(column_name,$1) from information_schema.columns
           where table_name = $2' using ',', target_table into cols;
  execute 'update ' || target_table || ' set (' || cols || ') =  select ($1).*
           where id = ($1).id' using NEW;
  return NULL;
end;
$$ language plpgsql;
Run Code Online (Sandbox Code Playgroud)

问题是update声明.我无法想出一个可以在这里工作的语法.我已经在PL/Perl中成功实现了这个,但是对plpgsql-only解决方案感兴趣.
有任何想法吗?

更新

正如@Erwin Brandstetter所说,这是我的PL/Perl解决方案的代码.我提出了他的一些建议.

create function f_tr_up() returns trigger as $$
  use strict;
  use warnings;
  my $target_table = quote_ident($_TD->{'table_name'}) =~ s/^([\w]+)_view$/$1/r;
  my $NEW = $_TD->{'new'};
  my $cols = join(',', map { quote_ident($_) } keys $NEW);
  my $vals = join(',', map { quote_literal($_) } values $NEW);
  my $query = sprintf(
    "update %s set (%s) = (%s) where id = %d",
    $target_table,
    $cols,
    $vals,
    $NEW->{'id'});
  spi_exec_query($query);
return;
$$ language plperl;
Run Code Online (Sandbox Code Playgroud)

Erw*_*ter 9

虽然@ Gary的答案在技​​术上是正确的,但他没有提到PostgreSQL 确实支持这种形式:

UPDATE tbl
SET (col1, col2, ...) = (expression1, expression2, ..)
Run Code Online (Sandbox Code Playgroud)

再次阅读本手册UPDATE.

完成动态SQL的工作仍然很棘手.由于您没有指定,我假设一个简单的情况,其中视图由与其基础表相同的列组成.

CREATE VIEW tbl_view AS SELECT * FROM tbl;
Run Code Online (Sandbox Code Playgroud)

问题

  • 特殊记录NEW在内部不可见EXECUTE.我NEW作为单个参数传递给USING子句EXECUTE.

  • 如上所述,UPDATElist-form需要单独的.我使用子选择将记录拆分为单独的列:

    UPDATE ...
    FROM  (SELECT ($1).*) x
    
    Run Code Online (Sandbox Code Playgroud)

    (括号周围$1不是可选的.)这允许我简单地使用string_agg()从目录表构建的两个列列表:一个列表和一个没有表格限定.

  • 无法将行值作为整体分配给各个列.手册:

    根据标准,目标列名称的带括号的子列表的源值可以是任何行值表达式,从而产生正确的列数.PostgreSQL只允许源值为行构造函数或子代SELECT.

  • INSERT实现更简单.假设视图和表的结构相同,我省略了列定义列表.(可以改进,见下文.)

我对你的方法做了一些更新,使它发光.

触发功能UPDATE:

CREATE OR REPLACE FUNCTION f_trg_up()
  RETURNS TRIGGER AS
$func$
DECLARE
   tbl  text := quote_ident(TG_TABLE_SCHEMA) || '.'
             || quote_ident(substring(TG_TABLE_NAME from '(.+)_view$'));
   cols text;
   vals text;
BEGIN
   SELECT INTO cols, vals
          string_agg(quote_ident(attname), ', ')
         ,string_agg('x.' || quote_ident(attname), ', ')
   FROM   pg_attribute
   WHERE  attrelid = tbl::regclass
   AND    NOT attisdropped   -- no dropped (dead) columns
   AND    attnum > 0;        -- no system columns

   EXECUTE format('
   UPDATE %s t
   SET   (%s) = (%s)
   FROM  (SELECT ($1).*) x
   WHERE  t.id = ($2).id'
   , tbl, cols, vals) -- assuming unique "id" in every table
   USING NEW, OLD;

   RETURN NEW;
END
$func$ LANGUAGE plpgsql;
Run Code Online (Sandbox Code Playgroud)

触发功能INSERT:

CREATE OR REPLACE FUNCTION f_trg_ins()
  RETURNS TRIGGER AS
$func$
DECLARE
    tbl text := quote_ident(TG_TABLE_SCHEMA) || '.'
             || quote_ident(substring(TG_TABLE_NAME from '(.+)_view$'));
BEGIN
   EXECUTE 'INSERT INTO ' || tbl || ' SELECT ($1).*'
   USING NEW;

   RETURN NEW;  -- don't return NULL unless you know what you're doing
END
$func$ LANGUAGE plpgsql;
Run Code Online (Sandbox Code Playgroud)

触发器:

CREATE TRIGGER trg_instead_up
INSTEAD OF UPDATE ON a_view
FOR EACH ROW EXECUTE PROCEDURE f_trg_up();

CREATE TRIGGER trg_instead_ins
INSTEAD OF INSERT ON a_view
FOR EACH ROW EXECUTE PROCEDURE f_trg_ins();
Run Code Online (Sandbox Code Playgroud)

SQL小提琴演示INSERTUPDATE.

主要观点

  • 包括模式名称以使表引用明确无误.在多个模式中,同一个数据库中可以有多个相同表名的实例!

  • 查询pg_attribute而不是information_schema.columns.这是移植性较差,但快,让我用表OID.

  • 当像SQLI一样处理动态SQL的查询时,表名对SQLi是不安全的.与逃避quote_ident()format()或与对象IDENTIFER类型.这包括特殊的触发函数变量TG_TABLE_SCHEMATG_TABLE_NAME!

  • 转换为对象标识符类型regclass以断言表名有效并获取目录查找的OID.

  • (可选)用于format()安全地构建动态查询字符串.

  • 对目录表的第一个查询不需要动态SQL.更快,更简单.

  • 除非您知道自己在做什么,否则请使用RETURN NEW而不是RETURN NULL在这些触发器功能中.(NULL将取消INSERT当前行.)

  • 这个简单的版本假定每个表(和视图)都有一个名为的唯一列id.更复杂的版本可能会动态使用主键.

  • 该函数UPDATE允许视图和表的列以任何顺序排列,只要该集合相同即可.INSERT期望视图和表的列的顺序相同的函数.如果要允许任意顺序,请在INSERT命令中添加列定义列表,就像使用UPDATE.

  • 更新版本还包括另外id使用对列的更改OLD.