Всем привет, сто лет у вас не был :)
Делаю проект на Firebird 2.03, нашёл нечто похожее на баг. Если это
что-то новое, м.б. добавите в баг-лист?
Итаг. Задача: нужно резервировать столики в ночном клубе на
определённую дату. Если столик на эту дату уже занят - ругать
пользователя последними словами. Особенность - если столик на эту дату
ещё не занят, но как раз в данный момент резервируется другим
оператором - ругать пользователя другими словами. Короче нужно
показывать статус столиков на дату - "свободен", "занят",
"резервируется" и позволять резервировать только те, у которых статус
"свободен".
Проблема: Dirty Read у нас нет и прочитать состояние "столика" до
коммита нельзя.
Решение: основываясь на том, что индексы работают вне контекста
транзакции, делаю проверку записи на уникальность ещё до её коммита.
Делаем таблицу (все операции проводил в IBExpert):
create table BoothReservation (
ID integer not null,
pDate date,
rBooth integer);
alter table BoothReservation
add constraint PK_BoothReservation
primary key (ID);
генератор и триггер для ID само собой разумеющиеся.
Накладываем ограничение "уникальность" на пару полей pDate, rBooth:
create unique index UNQ_BoothReservation
on BoothReservation (pDate, rBooth);
Запись о резервировании пишется в BoothReservation. По смыслу, если
записи для уникальной пары (pDate, rBooth) в BoothReservation нет -
столик "свободен", если есть - "занят". И если записи нет, но
проверочная вставка уникальной пары не проходит - столик
"резервируется".
Речь идёт именно о "проверке", когда юзеру нужно отобразить календарь
столиков с состояниями. Не то что бы мне нравилась проверка через
insert, но ничего умнее я не придумал.
Проверяем. Открываем 2 транзакции. В транзакции No.1 добавляем 2 записи:
insert into BoothReservation (pDate, rBooth) values ('9-JAN-2008', 5);
insert into BoothReservation (pDate, rBooth) values ('9-JAN-2008', 2);
commit;
В транзакции No.2 добавляем 1 запись:
insert into BoothReservation (pDate, rBooth) values ('9-JAN-2008', 2);
Ошибка:
Invalid insert or update value(s): object columns are
constrained - no 2 table rows can have duplicate column values.
attempt to store duplicate value (visible to active transactions) in
unique index "UNQ_BOOTHRESERVATION".
Всё правильно. Откатываем транзакцию No.2.
Проверяем без коммита. В транзакции No.1:
insert into BoothReservation (pDate, rBooth) values ('9-JAN-2008', 1);
В транзакции No.2:
insert into BoothReservation (pDate, rBooth) values ('9-JAN-2008', 1);
Ошибка:
Unsuccessful execution caused by system error that does not preclude
successful execution of subsequent statements.
lock conflict on no wait transaction.
attempt to store duplicate value (visible to active transactions) in
unique index "UNQ_BOOTHRESERVATION".
Тоже всё правильно. НЕ откатываем транзакцию No.2. В транзакции No.1
удаляем свежедобавленную, но не закоммиченую запись:
delete from BoothReservation where ID = 4;
НЕ коммитим транзакцию No.1. В транзакции No.2 пытаемся повторить вставку
той же записи:
insert into BoothReservation (pDate, rBooth) values ('9-JAN-2008', 1);
Ошибка:
Unsuccessful execution caused by system error that does not preclude
successful execution of subsequent statements.
lock conflict on no wait transaction.
attempt to store duplicate value (visible to active transactions) in
unique index "UNQ_BOOTHRESERVATION".
А вот это уже неправильно. Запись удалена, но транзакция No.2 почему-то
до сих пор не вкурсе. По логике раз уж уникальный индекс
перестраивается при вставке до коммита, значит при удалении тоже
должен (если вообще в перестраивании индекса дело).
Пробуем дальше. Откатываем транзакцию No.2 и открываем её заново (я
понимаю, что это будет уже No.3, но для удобства обхожусь тем же
номером :) ). Опять пытаемся повторить вставку той же записи:
insert into BoothReservation (pDate, rBooth) values ('9-JAN-2008', 1);
Та же ошибка. То есть дело точно не в "устаревшей" транзакции No.2.
Коммитим транзакцию No.1 (там висело незакоммиченое добавление и
удаление, если помните ещё :) ). В транзакции No.2 пытаемся повторить
вставку той же записи:
insert into BoothReservation (pDate, rBooth) values ('9-JAN-2008', 1);
Успешно.
Однако такой расклад меня не устраивает - в приложении я не могу
коммитить транзакцию No.1 раньше времени. Грубо говоря пока пользователь
не нажал в диалоге кнопку Ok или Cancel - транзакция не завершена. И
пользователь может выбирать в диалоге любые столик/дату, и в момент
выбора производится удаление предыдущего выбора из BoothReservation и
вставка нового. И всё это должно быть видно другим транзакциям без
коммита.
Ладно, исходя из предположения, что "индексы при удалении не сразу
перестраиваются" делаем финт ушами - перед удалением записи в
транзакции No.1 проводим апдейт:
update BoothReservation set pDate = (CURRENT_DATE - 365*100 - ID)
where ID = 4;
delete from BoothReservation where ID = 4;
Т.е. тупо апдейтим поле, входящее в уникальный индекс каким-нибудь
левым значением - в данном случае делаем минус 100 лет, минус ID
(чтобы разные записи не пересекались).
Работает. Теперь вставка "удалённой" записи в транзакции No.2 проходит
без коммита транзакции No.1. Что и требовалось получить.
Но баг остаётся багом :)
Server version: WI-V6.3.3.12981 Firebird 2.0, Classic Server.
ODS 11.0
Client library version: 2.0.3.12981
WinXP SP2, NTFS :)