7

给定以下架构:

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;

我尝试在 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();

我的一个不成功的尝试是:

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;

问题在于update声明。我无法想出在这里可以使用的语法。我已经在 PL/Perl 中成功实现了这一点,但会对仅 plpgsql 的解决方案感兴趣。
有任何想法吗?

更新

正如@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;
4

2 回答 2

14

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

UPDATE tbl
SET (col1, col2, ...) = (expression1, expression2, ..)

再次阅读手册。UPDATE

使用动态 SQL 完成他的工作仍然很棘手。由于您没有指定,我假设视图包含与其基础表相同的列。

CREATE VIEW tbl_view AS SELECT * FROM tbl;

问题

  • 里面的特殊记录NEW是不可见的EXECUTE。我NEW使用. USING_EXECUTE

  • 如前所述,UPDATE列表形式需要单独的。我使用子选择将记录拆分为单独的列:

    UPDATE ...
    FROM  (SELECT ($1).*) x
    

    (括号$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;

触发功能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;

触发器:

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

SQL Fiddle演示INSERTUPDATE.

要点

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

  • 查询pg_attribute而不是information_schema.columns. 这不太便携,但速度更快,并且允许我使用 table-OID。

  • 当将表名作为字符串处理时,例如在为动态 SQL 构建查询时,表名对SQLi是不安全的。quote_ident()使用或format()使用对象标识符类型进行转义。这包括特殊的触发函数变量TG_TABLE_SCHEMATG_TABLE_NAME

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

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

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

  • 除非您知道自己在做什么,否则在这些触发函数中使用RETURN NEW而不是。RETURN NULLNULL将取消INSERT当前行的。)

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

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

  • 更新版本还涵盖了id通过OLD额外使用对列的更改。

于 2013-03-12T00:32:28.600 回答
1

Postgresql 不支持使用set (col1,col2) = select val1,val2语法更新多个列。

为了在 postgresql 中达到同样的效果,您可以使用

update target_table
set col1 = d.val1,
    col2 = d.val2
from source_table d
where d.id = target_table.id

这将使动态查询的构建更加复杂,因为您需要将正在使用的列名列表迭代到各个字段中。我建议您使用array_agg而不是string_agg作为数组比再次拆分字符串更容易处理。

Postgresql 更新语法

关于 array_agg 函数的文档

于 2013-03-11T16:09:39.443 回答