在 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 对此没有很好的支持,主要问题是您在 before insert
触发器中对同一表执行的 insert
将导致调用同一触发器。 @Laurenz Albe 建议如何摆脱无限循环,虽然建议的技术(巧妙!)看起来是一件值得记住的好事,但我们不知道对性能或其他副作用的可能影响。
最后,@Ilya Dyoshin 提议只从应用程序中调用一个包含必要 SQL 逻辑的函数,一针见血。我觉得这是一个双赢的解决方案,因为
1) 它不会将 insert into x
for table x
的语义更改为“really mean update
, sometimes”;
2) “upsert 语义”在应用程序代码中是明确的,但没有详细说明;
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,其中一个一定会失败。
关于sql - 如何在 PostgreSQL 中编写 upsert 触发器?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47562240/