ОБНОВЛЕНИЕ: я устранил Hibernate из проблемы. Я полностью переработал описание проблемы, чтобы максимально упростить ее.
У меня есть таблица master
с триггером noop и таблица detail
с двумя отношениями между таблицами master
и detail
:
create table detail (
id bigint not null,
code varchar(255) not null,
primary key (id)
);
create table master (
id bigint not null,
name varchar(255),
detail_id bigint, -- "preferred" detail is one-to-one relation
primary key (id),
unique (detail_id),
foreign key (detail_id) references detail(id)
);
create table detail_candidate ( -- "candidate" details = many-to-many relation modeled as join table
master_id bigint not null,
detail_id bigint not null,
primary key (master_id, detail_id),
foreign key (detail_id) references detail(id),
foreign key (master_id) references master(id)
);
create or replace function trgf() returns trigger as $$
begin
return NEW;
end;
$$ language 'plpgsql';
create trigger trg
before insert or update
on master
for each row execute procedure trgf();
insert into master (id, name) values (1000, 'x'); -- this is part of database setup
insert into detail (code, id) values ('a', 1); -- this is part of database setup
В такой настройке я открываю два окна терминала с помощью psql
и выполняю следующие шаги:
- в первом терминале смените мастер (оставьте транзакцию открытой)
begin;
update master set detail_id=null, name='y' where id=1000;
- во втором терминале добавьте подробный кандидат в мастера в собственной транзакции
begin;
set statement_timeout = 4000;
insert into detail_candidate (master_id, detail_id) values (1000, 1);
Последняя команда во втором тайм-ауте терминала с сообщением
ERROR: canceling statement due to statement timeout
CONTEXT: while locking tuple (0,1) in relation "master"
SQL statement "SELECT 1 FROM ONLY "public"."master" x WHERE "id" OPERATOR(pg_catalog.=) $1 FOR KEY SHARE OF x"
Мои наблюдения и вопросы (изменения независимы):
- когда БД настроен без триггера, т.е. после начальной настройки вызывается
drop trigger trg on master;
, все работает нормально. Почему наличие noop триггера имеет такое влияние? Я не понимаю. - когда БД настроен без уникального ограничения на
master.detail_id
(то естьalter table master drop constraint master_detail_id_key;
вызывается после начальной настройки), все тоже работает нормально. Почему? - Когда я опускаю явное назначение
detail=null
в операторе обновления в первом терминале (так как в любом случае в настройке есть нулевое значение), все тоже работает нормально. Почему?
Пробовал на Postgres 9.6.12 (встроенный), 9.6.15 (в Docker), 11.5 (в Docker).
Проблема воспроизводима в образе Docker tomaszalusky/trig-example
, который доступен на DockerHub или может быть создан из этого файла Docker (инструкции внутри).
ОБНОВЛЕНИЕ 2: Я обнаружил обычное поведение трех наблюдений выше. Я породил запрос select * from pgrowlocks('master')
из расширения pgrowlocks во второй транзакции. Блокировка на уровне строк обновленной строки в master
- это FOR UPDATE
в случае сбоя, но FOR NO KEY UPDATE
во всех трех рабочих случаях. Это полностью соответствует таблице соответствия режима в документации, поскольку режим FOR UPDATE
является более сильным, а режим, запрашиваемый оператором вставки, - FOR KEY SHARE
(что видно из сообщения об ошибке, также вызывающего select ... for key share
команда имеет тот же эффект, что и команда insert
.
Документация по режиму FOR UPDATE
гласит:
Режим блокировки FOR UPDATE также получает (...) UPDATE, который изменяет значения в определенных столбцах. В настоящее время для случая UPDATE рассматриваются столбцы с уникальным индексом, который можно использовать во внешнем ключе (...)
Это верно для столбца master.detail_id
. Однако все еще неясно, почему режим FOR UPDATE
не выбирается независимо от наличия триггера и почему наличие триггера вызвало его.