如何在 PostgreSQL 中编写 upsert 触发器?

How to write an upsert trigger in PostgreSQL?

在 PostgreSQL 9.6 及更高版本中,定义触发器函数的正确方法是什么? 每当插入因唯一性约束而失败时更新?

我知道写 insert ... on conflict ... do update set ... 语句很简单,但是我的 我的想法是,我想要一些 table 将重复插入视为更新;否则那件 逻辑必须由应用程序而不是数据库来处理。

我找到的一个表面上有效的解决方案是:

create table versions (
  key           text primary key,
  version       text );

/* ### TAINT not sure whether there may be race conditions with this upsert trigger */
create function on_before_insert_versions() returns trigger language plpgsql volatile as $$ begin
  if exists ( select 1 from versions where key = new.key ) then
    update versions set version = new.version where key = new.key;
    return null;
    end if;
  return new;
  end; $$;

create trigger on_before_insert_versions
  before insert on versions for each row execute procedure on_before_insert_versions();

insert into versions values
  ( 'server', '3.0.3' ),
  ( 'api',    '2' );

insert into versions values
  ( 'api',    '3' );

select * from versions;

  key   | version 
--------+---------
 server | 3.0.3
 api    | 3

但是,触发器是否容易出现竞争条件?我试着用一个 insert ... on conflict ... do update set ... 触发器中的语句,但当然失败了 因为它自己触发触发函数,导致无限倒退。

我还尝试使用一对 alter table ... disable trigger ... / enable 语句,但是 cannot ALTER TABLE ... because it is being used by active queries in this session.

错误

在唯一性约束下始终执行更新而不是插入的规范形式是什么 Postgre 中的违规行为SQL?

Update—PostgreSQL 中的更新,或者他们长期缺席,是一个热门话题,许多不那么完美的解决方案经常出现建议。

鉴于 Postgres 的维护者已经花费了如此多的时间和精力来使 insert ... on conflict .. do update 在没有竞争条件的情况下工作,接受 'seems to work' 的自制解决方案可能是不明智的(直到它不).

当我写下我的问题时,我坚持要有一个 insert 触发器在冲突时执行 update; PostgreSQL 对此没有很好的支持,主要问题是你在 before insert 触发器中对同一个 table 执行的 insert 将导致同一个触发器叫。 @Laurenz Albe 建议如何摆脱无限循环,虽然建议的技术(巧妙!)看起来是一件值得记住的好事,但我们不知道对性能或其他副作用的可能影响。

最后,@Ilya Dyoshin 提议只从应用程序中调用一个包含必要 SQL 逻辑的函数,一针见血。我觉得这是一个 win/win 解决方案,因为

1) 它不会将 insert into x for table x 的语义更改为 'really mean update, sometimes';

2) 'upsert semantics' 在应用程序代码中明确说明,但没有详细说明;

3) 您可以 仍然 执行 insert 而无需隐式 'update' — 事后看来,这可能是最重要的考虑因素。

最好使用纯upsert

否则你可以引入更复杂的逻辑,并且不要 return 从触发器插入的数据(阅读文档 = 如果插入前的触发器不是 returning 值,则不执行插入)

我同意 Ilya 的观点,即在应用程序中以直接的方式执行此操作会更好。

但我本着思想实验的精神来看待它,我的解决方案利用 pg_trigger_depth() 的力量来逃避无休止的递归:

CREATE OR REPLACE FUNCTION on_before_insert_versions() RETURNS trigger
   LANGUAGE plpgsql AS
$$BEGIN
   IF pg_trigger_depth() = 1 THEN
      INSERT INTO versions (key, version) VALUES (NEW.key, NEW.version)
         ON CONFLICT (key)
         DO UPDATE SET version = NEW.version;
      RETURN NULL;
   ELSE
      RETURN NEW;
   END IF;
END;$$;

您的解决方案绝对容易受到竞争条件的影响:两个并发的 INSERT 可能导致并发 运行 触发器,两者都无法在 versions 中找到匹配的行,因此导致 INSERT,其中之一必须失败。