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

时间:2017-11-29 21:37:12

标签: sql postgresql upsert

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

我知道写insert ... on conflict ... do update set ...语句是直截了当的,但是我的 我的想法是,我希望有一些表将重复插入视为更新;否则那件 逻辑必须由应用程序处理,而不是数据库。

我找到的一个解决方案就是:

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

始终执行更新而不是插入唯一性约束的规范表单是什么 PostgreSQL中的违规行为?

更新 - PostgreSQL中的插件,或者他们长时间缺席,都是一个热门话题,并且经常提出许多不那么完美的解决方案。

鉴于Postgres维护者花了很多时间和精力让insert ... on conflict .. do update在没有竞争条件的情况下工作,所以接受一个“似乎有用”的自制解决方案(直到它没有)可能是不明智的。

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

最后,@ Ilya Dyoshin提出要从包含必要SQL逻辑的应用程序调用函数。我觉得这是一个双赢的解决方案,因为

1)它不会将表格insert into x的{​​{1}}的语义更改为“真正意味着x,有时会更改”;

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

3)你可以仍然update而不打算隐含'更新' - 事后看来,这可能是最重要的考虑因素。

2 个答案:

答案 0 :(得分:3)

我同意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,其中一个必须失败。

答案 1 :(得分:1)

最好使用纯upsert。

否则你可以引入更复杂的逻辑,不要从触发器返回插入数据(在插入不返回值之前读取docs = if触发器,不执行插入)