Генераторы и их использование

Кузьменко Дмитрий, написано – март 1997, переписано – 26.08.2005.
upd: 12.10.2022

Большинство SQL-серверов имеет специальные механизмы для создания уникальных идентификаторов – autoincrement, identity, sequence, и т. п. В InterBase и Firebird для этого существует механизм генераторов.

В данной статье будут рассмотрены следующие темы:  

Введение

Генераторы предназначены для получения последовательностей уникальных чисел. Например, 1, 2, 3..., 10, 20, 30 и т. п. Такие числа обычно используются как идентификаторы записи в таблицах, имеющих суррогатный (абстрактный)LINK первичный ключ. Например, в таблице клиентов для их нумерации введен столбец CLIENT_ID INTEGER, по которому построен первичный ключ. Столбец можно заполнять значениями генератора.

Нужно сразу заметить, что сами по себе генераторы не обеспечивают сохранение последовательности номеров в случае удаления записей – генератор всего лишь выдает числа по очереди увеличивая их на некоторую величину и обеспечивая уникальность выданных значений. То есть, генератор выглядит как переменная типа integer (в первом диалекте, или int64 в третьем диалекте), которая находится в памяти, и над которой можно выполнять операции Inc и Dec. Если вам требуется обеспечить непрерывные последовательности идентификаторов записей даже в случае их удаления или модификации, то вам нужно обратиться к статье Auditable series of numbers (непрерывные последовательности чисел). В любом случае это задача приложения и сервера, и выходит за рамки данного описания.
 

Создание генераторов

Генератор – это специальный объект базы данных, который генерирует уникальные последовательные числа. Эти числа могут быть использованы в качестве идентификаторов (например код клиента, номер счета и т. п.). Для создания генератора необходимо использовать оператор DDL
CREATE GENERATOR generatorname;

В Firebird 2.5 альтернативой слову generator является слово sequence.

При выполнении такой команды происходит 2 действия:
  1. На специальной странице БД для хранения значения генератора отводится
    • 4 байта (в первом диалекте или до InterBase 6.0) или
    • 8 байт (в третьем диалекте и во всех современных версиях InterBase/Firebird/Yaffil)
  2. В системной таблице RDB$GENERATORS создается запись, куда помещается имя генератора и его номер (фактически смещение на странице генераторов).
После создания генератора его значения можно получать при помощи функции
GEN_ID(generatorname, inc_value)
где inc_value – число, на которое необходимо прирастить значение генератора.
 
Внимание! Получить новое значение генератора можно
1. оператором select, выбрав значение gen_id из таблицы с одной записью, например, системной rdb$database:
select gen_id(my_gen, 1) from rdb$database
2. в триггерах и процедурах – просто присвоив значение gen_id переменной:
myvar=gen_id(my_gen, 1);
3. в запросах – просто вызовом функции gen_id(my_gen, 1)
Генераторы возвращают значения (и сохраняют свои значения на диске) вне контекста транзакции пользователя. Это означает, что если значение генератора было увеличено с 10 до 11 (инкремент 1), то даже при откате транзакции (ROLLBACK) значение генератора, выданное в этой транзакции, не вернется к предыдущему. Вместе с этим гарантируется, что каждому пользователю будет всегда возвращено уникальное значение генератора (вызов функции gen_id всегда происходит "монопольно", то есть непараллельно для любого числа одновременных вызовов, чтобы исключить возможность получения разными пользователями одного и того же значения).
 
Примечание. Приращение значения генератора производится монопольно, аналогично функции InterlockedIncrement в Win32 API. То есть, даже если 1000 пользователей одновременно вызовут gen_id(x, 1), то они получат каждый свое (уникальное) значение (от x+1 до x+1+1000).
При выборке значения генератора запросом вида select gen_id(genname, x) from ... следует учитывать буферизацию выборки на клиенте. Т.е. в многопользовательской среде при выполнении двух таких запросов значения генератора будут увеличиваться "пачками", а не на величину x для каждой выбираемой записи.
 
Примечание. Иногда находятся умельцы, которые пишут
select gen_id(a, 0) + 1 from rdb$database
или
tmp=gen_id(a, 0);
tmp=tmp+1;
и др. аналогичные варианты. Если непонятно, почему так делать нельзя, прочитайте примечание выше еще раз.
 

Использование генераторов

Работа с генератором возможна только при помощи встроенной функции gen_id, а значит работать с генераторами можно в запросах, процедурах и триггерах. Давайте создадим генератор, и посмотрим, как его можно использовать.
CREATE GENERATOR NEWID;

В этот момент в базе создан новый генератор. Его значение равно 0. Если вам нужно использовать другое начальное значение, то можно установить (изменить) значение генератора следующей командой
SET GENERATOR NEWID 1000;

Выполнять данную операцию во время работы пользовательских приложений можно только в том случае, если новое значение генератора будет заведомо больше текущего. В противном случае приложения могут перестать нормально работать из-за конфликтов новых значений генератора с текущими значениями идентификаторов.

Обычно "начальное" значение генератора устанавливают в отличное от нуля, когда база данных является "распределенной", то есть в разных филиалах могут создавать новые записи в какой-либо таблице. Например, в главном офисе нумерация начинается с 0, в первом филиале – с 100000, во втором – с 200000, и так далее. Такой подход позволит избежать ситуаций, когда в двух филиалах созданы разные записи с одинаковым идентификатором.

Другой случай – наличие готового справочника с фиксированными идентификаторами. В этом случае вам нужно установить значение генератора больше максимального существующего идентификатора такого справочника.

В общем, установка начального значения генератора <> 0 зависит только от специфических требований приложения.

Увеличение значения генератора

Выше уже было сказано, что функция GEN_ID действует аналогично функции Inc, то есть увеличивает значение генератора на заданное число. Обычно инкремент значения используют равный 1
GEN_ID(NEWID, 1);

В определенных случаях может потребоваться больший шаг значений, например 10, если в системе возможны ситуации, когда нужно "вставить" идентификатор между двумя соседними. В этом случае шаг генератора выбирается > 1, а для вставки идентификатора "посередине" ему присваивается промежуточное число, без использования генератора. Например, в таблице автоматически назначены идентификаторы 10, 20 и 30. Для "вставки" нового идентификатора между 10 и 20 ему присваивается номер 15 (уже без использования генераторов, естественно).

Увеличение значения генератора функцией GEN_ID производится в монопольном режиме. Это означает, что одновременный вызов GEN_ID(NEWID, 1) двумя приложениями вернет каждому только свой идентификатор. Пример:
генератор имеет значение 7
приложение1: gen_id(newid, 1) -- выдан номер 8
приложение2: gen_id(newid, 1) -- выдан номер 9
...

Select

Наиболее распространенным и правильным использованием генераторов является "получение" нового идентификатора в клиентское приложение, и его последующее использование для тех или иных нужд. Это можно сделать вызовом
SELECT GEN_ID(NEWID, 1) FROM RDB$DATABASE

rdb$database здесь используется как таблица, содержащая только одну запись. В результате этим запросом также будет выдана 1 запись со значением генератора, увеличенным на 1.

Такой же способ используется в компонентах IBX LINK и FIBPlus – у IBDataSet есть метод GeneratorField, который позволяет обеспечить автоматическое присвоение нового номера столбцу первичного ключа записи как раз при помощи указанного оператора select, выполняемого библиотекой компонент "прозрачно". В FIBPlus та же самая функциональность устанавливается при помощи FIBDataSet.AutoUpdateOptions (GeneratorName).

Если вы по каким то причинам не хотите или не можете пользоваться методом GeneratorName, то можно взять отдельный компонент IBSQL (или IBQuery), прописать в нем запрос получения нового значения генератора, и вызывать такой запрос перед вставкой новой записи в таблицу.

Триггер

Однако, начинающие разработчики чаще пытаются использовать генератор в триггере, примерно следующим способом:
CREATE TRIGGER TBI_CLIENTS FOR CLIENTS
ACTIVE BEFORE INSERT POSITION 0
AS
BEGIN
NEW.CLIENT_ID = GEN_ID(NEWID, 1);
END
то есть, новый идентификатор присваивается автоматически при вставке записи. Такой способ вполне нормален, однако он подходит только для случаев, когда приложение вставляет запись и дальше уже не интересуется ею. Дело в том, что при многопользовательской работе и данном способе невозможно узнать, какое именно значение генератора было присвоено столбцу при вставке. Следовательно, если вам (или используемым компонентам доступа) нужно знать созданный идентификатор, то следует воспользоваться методом, изложенном выше в разделе SELECT.

Конечно, триггер можно оставить, изменив лишь код
IF (NEW.CLIENT_ID IS NULL) THEN
NEW.CLIENT_ID = GEN_ID(NEWID, 1);
чтобы если никакое значение столбца CLIENT_ID при вставке записи из приложения не передано, то оно было сгенерировано автоматически.
 
Примечание. Первая попытка перенести ответственность за автоматическую нумерацию столбца первичного ключа таблицы обычно проваливается из-за компонент доступа. Поскольку такой столбец объявлен как not null, и компоненты автоматически считывают характеристики столбцов, у TField будет установлено в True свойство Required. Это не дает возможности оставить столбец "пустым" при передаче с клиента на сервер. Установите свойство Required у такого столбца в False.
Примечание. Данная проблема может быть решена в Firebird 2.0, при помощи оператора INSERT...RETURNING. Однако если вручную (через IBSQL, IBQuery) такой оператор можно выполнить и получить из него результат, то для IBDataSet это может оказаться невозможным, поскольку IBDataSet или IBQuery+IBUpdateSQL просто не будут знать, что оператор вставки что-то возвращает. Если от FIBPlus можно ожидать поддержки insert...returning после выхода Firebird 2.0, то от IBX – вряд ли, если только аналогичная функциональность будет реализована в InterBase.

Процедура

Использование генераторов в процедурах ничем не отличается от использования генераторов в триггерах. Вот пример процедуры, которая возвращает новое значение генератора:
CREATE PROCEDURE GETNEWCLIENT
RETURNS (NID INTEGER)
AS
BEGIN  
NID = GEN_ID(NEWCLIENT, 1);
SUSPEND;
END

Чтобы получить результат такой процедуры, можно ее выполнить как
SELECT NID FROM GETNEWCLIENT

Также, если не указывать в процедуре SUSPEND, можно получить значение из процедуры путем
EXECUTE PROCEDURE GETNEWCLIENT RETURNING_VALUES :param

Этот способ отличается от обращения к rdb$database тем, что вам приходится создавать новую процедуру на каждый генератор. Если вдруг потребовалось создать одну процедуру для получения новых значений из разных генераторов, то можно воспользоваться двумя способами. Первый способ – просто обработка if
CREATE PROCEDURE GETNEWID
(GENID INTEGER)
RETURNS (NID INTEGER)
AS
BEGIN  
IF (:GENID = 1) THEN
NID = GEN_ID(NEWCLIENT, 1);
IF (:GENID = 2) THEN
NID = GEN_ID(NEWORDER, 1);
IF (:GENID = 3) THEN
NID = GEN_ID(NEWSALE, 1);
...
END

Можно такую процедуру более "универсализировать" при помощи оператора EXECUTE STATEMENT в Firebird 1.5, который позволяет выполнять запросы динамически в процедурах и триггерах.
CREATE PROCEDURE GETNEWID
(GEN VARCHAR(30))
RETURNS (NID BIGINT)
AS
DECLARE VARIABLE SQL VARCHAR(60);
BEGIN
SQL = 'SELECT GEN_ID('||GEN||', 1) FROM RDB$DATABASE;
EXECUTE STATEMENT SQL INTO :NID;
SUSPEND
END
 
Обратите внимание, что возвращаемая переменная NID имеет тип BIGINT, в отличие от предыдущих примеров – execute statement требует точного соответствия типов переменных. Если требуется получить результат в тип integer, то следует применить операцию cast(var as integer) либо сразу в запросе, либо к выходной переменной после того, как значение получено из execute statement.

Следует помнить, что execute statement производит динамическое выполнение запроса, а значит это будет чуть медленнее, чем выполнение такого запроса с клиента. Пример вызова процедуры (suspend в процедуре опять же указан для возможности ее вызова как select):
SELECT * FROM GETNEWID('NEWCLIENT');
SELECT * FROM GETNEWID('NEWORDER');
SELECT * FROM GETNEWID('NEWSALE');

Как видите, этот изощренный способ опять же ничем не лучше, чем просто вызовы
SELECT GEN_ID(NEWCLIENT, 1) FROM RDB$DATABASE;
SELECT GEN_ID(NEWORDER, 1) FROM RDB$DATABASE;
...

Экзотические "глюки"

  1. Экзотическим применением генератора является например его указание в default для столбца таблицы. В результате при restore сервер будет вызывать default столько раз, сколько есть записей в таблице, и значение генератора после restore окажется равным старому (до backup) плюс число записей в таблице. Данное поведение справедливо для всех версий InterBase/Firebird/Yaffil, и будет исправлено в Firebird 2.0.
  2. "Двоение" генераторов, то есть увеличение не на +1, а на +2, можно элементарно получить, если
    • вы сначала использовали генератор в триггере (без условия if)
    • прочитав эту статью, решили использовать select gen_id(x, 1) from rdb$database
то есть, сначала генератор будет инкрементироваться приложением, а потом еще и триггером. Поэтому, проверьте, содержит ли данный триггер проверку столбца на is null, как это указано ранее в статье.
  1. Вызов gen_id(x, y) в процедуре или триггере всегда возвращает 0.
Это может быть связано с тем, что генератор удален и создан снова. При этом в коде blr процедуры или триггера, которая явно ссылается на имя генератора, осталась ссылка на старый идентификатор генератора, в то время как в rdb$generators этот генератор уже имеет новый идентификатор.
Для решения этой проблемы нужно сделать alter trigger/procedure, чтобы перекомпилировался blr.
 
Примечание. Данная проблема вовсю существовала во времена повсеместного использования SQLExplorer (инструмента BDE). Он при попытке изменить значение генератора почему то удалял и создавал генератор снова.


Сброс значения генератора

Переустановка значения генератора на какое то новое значение, которое отличается от его обычного инкремента, может производиться теми же способами, которые были указаны выше – как при помощи оператора SET GENERATOR так и при помощи функции GEN_ID.

Такие операции чаще выполняют как установку значений генераторов в некие начальные, при установке системы на новый сервер (филиал, подразделение, однопользовательское рабочее место и т. п.). Следует категорически избегать таких действий во время работы в многопользовательской среде, т. к. это нарушит нормальную работу приложений (генератор может начать выдавать значения, уже существующие в таблицах).
SET GENERATOR NEWCLIENT TO 0;

Это обычный ddl оператор. Он не может быть выполнен в процедуре или триггере (за исключением определенных версий Yaffil). Если все же сброс генератора требуется совершить в некоей процедуре (как уже говорилось, в триггерах такие действия выполнять категорически не рекомендуется), то это можно выполнить при помощи gen_id:
...
TEMPVAR = GEN_ID(NEWCLIENT, -GEN_ID(NEWCLIENT, 0));
...
то есть, генератор увеличивается на его же текущее отрицательное значение.
 

Получение "текущего" значения генератора

Под "текущим" имеется в виду сиюминутное значение генератора. Но если работа производится в многопользовательской среде, то слово "текущее" приобретает и второй смысл – то, которое постоянно меняется. Конечно, раз можно увеличить значение генератора на любое число, почему бы его не увеличить на 0?
SELECT GEN_ID(NEWCLIENT, 0) FROM RDB$DATABASE

Результатом будет значение генератора, которое существовало на момент вызова этого запроса, ни больше ни меньше. В следующий момент этот же запрос может показать другое значение (если другие приложения увеличивают или уменьшают значение конкретного генератора в данный момент). Поэтому подобную операцию следует выполнять только для получения информации, в каком состоянии в данный момент находится конкретный генератор, но не для использования полученного значения в приращениях, вставках, обновлениях и т. п. (см. дальше "Типичные ошибки в использовании генераторов").

Получение списка генераторов и их значений

Проблема тут в том, что значения генераторов находятся на специальных страницах БД, и не видны никаким образом без обращения к конкретному генератору. То есть, значения генераторов в rdb$generators вы не увидите. Поэтому, для получения списка генераторов и их текущих значений, например, в виде скрипта, придется написать блок кода, причем, с execute statement, потому что мы не можем передать имя объекта в переменной, как параметр:
(блок кода для 3.0, может быть работает и в 2.5)
 
execute block
returns (v varchar(100))
as
declare variable s varchar(100)='';
declare variable n varchar(100)='';
begin
 for select g.rdb$generator_name from rdb$generators g
   where g.rdb$generator_name not starting with 'RDB$' and
   g.rdb$generator_name not starting with 'SQL$'
  into :n
   do
    begin
       s='select gen_id('||:n||', 0) from rdb$database';
       execute statement (s) into :v;
       v='set generator '||trim(:n)||' to '||:v||';';
       suspend;
    end
end

Если нужно просто вывести имена и значения генераторов в виде "таблицы", то вот кусок кода попроще:
execute block
returns (gname varchar(32), gvalue bigint)
as
declare variable v varchar(200);
begin
  for select rdb$generator_name from rdb$generators
  where rdb$generator_name not starting with 'RDB$'
  into :gname
  do
    begin
      v=:gname;
      v='select gen_id('||v||', 0) from rdb$database';
      execute statement (v) into :gvalue;
      suspend;
    end
end

Удаление генераторов

Оператор DROP GENERATOR X появился только в Firebird 1, Yaffil и InterBase 7.1. В предыдущих версиях в языке DDL оператора для удаления генератора не было. Тем не менее, серьезной проблемы это не представляло. В самом начале статьи было упомянуто, что запись о генераторе создается в таблице RDB$GENERATORS. Эту запись, безусловно, можно удалить. Однако место, распределенное на странице генераторов, освобождено не будет. Оно будет освобождено (точно так же и для drop generator) только после того, как вы сделаете вашей БД BACKUP/RESTORE.
 

Backup/Restore

Значения генераторов сохраняются при backup, и восстанавливаются при restore. Однако, если делать бэкап только метаданных (или restore только метаданных), то в некоторых версиях Interbase/Firebird/Yaffil значения генераторов могут оказаться "мусорными". Эта ошибка исправлена в самых последних версиях Firebird и Yaffil.

Если backup/restore метаданных используется для создания "клона" базы данных, используемого в другой фирме, офисе и т. п., то вам придется самостоятельно или обнулить генераторы, или установить их в требуемые значения.
 

Сбои в работе

Как уже говорилось выше, генераторы не подвержены влиянию транзакций, и казалось бы, должны инкрементироваться всегда. Увы, иногда при сбоях сервера (выключение питания, падение ОС и т. п.) страницы генераторов не успевают сохраниться на диск, хотя данные, записанные по commit, сохраняются. Более вероятна такая ситуация при Forced Write = Off.

В результате, после рестарта сервера, в базе могут оказаться "старые" значения генераторов, при том что данные уже содержат идентификаторы, полученные от "новых" значений.

Это исправлено в Firebird 1.5.2, то есть страницы генераторов записываются на диск до записи страниц с данными. На других версиях InterBase/Firebird/Yaffil с данной проблемой можно бороться только написав специальную программу "восстановления" или контроля значений генераторов после сбоя, которая будет содержать проверку на select max(id) from table и установку соответствующего генератора в это значение.
 

Типичные ошибки в использовании генераторов

При использовании генераторов в процедурах и триггерах разработчик иногда забывает, что в них может быть использован процедурный язык, и постоянно получает новое значение генератора путем обращения к rdb$database
declare variable i int;
begin
SELECT GEN_ID(X, 1) FROM RDB$DATABASE
INTO :I;
INSERT INTO TBL VALUES (:I...
...
end

В данном случае в select нет никакой необходимости. Текст можно сильно упростить
declare variable i int;
begin
I = GEN_ID(X, 1);
INSERT INTO TBL VALUES (:I...
...
end

А если новое значение генератора не требуется использовать дальше по ходу процедуры, то можно и еще проще
begin
INSERT INTO TBL VALUES (GEN_ID(X, 1), ...
...
end

Собственно, ошибкой первый пример кода не является.

Встречаются случаи избыточного преклонения перед примером с rdb$database:
insert into table (field1, field2...)
values (select gen_id(mygen, 1)from rdb$database, :param... )

Помилуйте, gen_id – это функция, поэтому здесь достаточно такого:
insert into table (field1, field2...)
values (gen_id(mygen, 1), :param... )

Собственно, такой вызов gen_id не приветствуется – нет гарантий, что вставка в эту таблицу будет производится только этим оператором, прописанным в одном единственном месте кода вашего приложения (опять же единственного, работающего с этой БД).

Более существенная и грубая ошибка в использовании генераторов – это увеличение "текущего" значения генератора вне контекста gen_id. Схематично выглядит как
I = GEN_ID(X, 0);
I = I + 1;
или еще более безумный вариант
SELECT GEN_ID(X, 0) + 1 FROM RDB$DATABASE

Понятно, что время между получением текущего значения генератора и его увеличением очень мало, но все равно нельзя исключать, что в многопользовательской среде два таких блока кода, выполняемые параллельно, могут получить одно и то же значение генератора (в во втором случае вероятность куда больше, чем в первом, потому что неизвестно, когда ваш код поместит полученное таким образом значение на сервер). В начале статьи было указано, что функция GEN_ID гарантирует получение уникального значения при инкременте <> 0 путем "монополизации" вызова функции с использованием объектов операционной системы типа mutex. В приведенных вариантах защиты от параллельного вызова нет.

Как интересный пример самостоятельного использования mutex при необходимости обеспечения сложной последовательности увеличения значений, можете посмотреть udfdemox. Используемая в нем функция mutex упоминается дальше по тексту статьи.
 

Нестандартное применение генераторов

Вы уже видели, что функцию GEN_ID можно использовать в операторе SELECT. Вот как можно получить количество записей, выбранных запросом:
SET GENERATOR MYGEN TO 0;
SELECT GEN_ID(MYGEN, 1), FIELD1, FIELD2, FIELD3, ... FROM MYTABLE.

Такой запрос вернет в качестве первого поля порядковый номер записи, и после выполнения запроса генератор MYGEN будет содержать количество возвращенных записей. Кроме этого, во время выполнения этого запроса любой другой пользователь этой же БД может получить текущее значение генератора MYGEN и узнать сколько записей уже выбрано запросом на текущий момент (нечто вроде ProgressBar, однако число записей все-равно неизвестно до окончания выполнения запроса).

Понятно, что такой способ невозможно использовать для нескольких приложений одновременно, т. к. в этом случае значения генератора будут увеличиваться скачками.

Функцию GEN_ID можно также использовать и как "выключатель" длительных запросов. Пример приведен для БД EMPLOYEE.GDB.
SET GENERATOR EMP_NO_GEN TO 0;
SELECT * FROM EMPLOYEE, EMPLOYEE, EMPLOYEE
WHERE GEN_ID(EMP_NO_GEN, 0) = 0;

Фактически такой запрос означает – "выбирать записи пока значение генератора = 0". Как только другой пользователь или ваше приложение в другом коннекте выполнит операцию
SET GENERATOR EMP_NO_GEN TO 1;
запрос прекратится, т. к. условие WHERE станет равным FALSE.

Обязательно учтитывайте буферизацию записей клиентской частью (gds32.dll) или сервером при выполнении подобных запросов. Например, приведенный выше запрос с проверкой генератора в where "выключится" не сразу, а через некоторое время. Перед применением такого метода в вашей системе сначала следует проверить, подходит ли он вообще для выполняемого вами запроса.

Безусловно, в многопользовательской среде невозможно использовать в таких целях один и тот же генератор. Для решения этой проблемы можно завести глобальный генератор, который будет выдавать уникальные идентификаторы пользователям при коннекте, а клиентское приложение будет запоминать его номер и хранить на локальном компьютере для последующего использования. Логика работы может быть следующая:
  • Клиентское приложение при запуске определяет, есть ли для него (например в Registry или INI-файле) "именной" генератор.
  • Если нет, то оно операцией SELECT GEN_ID(GlobalGen, 1) FROM RDB$DATABASE получает идентификатор (например 150), создает на сервере собственный генератор операцией CREATE GENERATOR USER_N; (например, USER150). После чего сохраняет имя этого генератора на локальном диске.
  • Если да, то приложение обнуляет "именной" генератор операцией SET GENERATOR ... TO 0; (в нашем примере SET GENERATOR USER150 TO 0;) и выдает запросы с использованием данного генератора.
Этот способ не может быть рекомендован для систем с большим числом пользователей или частыми коннектами-дисконнектами. В ранних версиях InterBase был баг, который приводил к ошибке при определенном числе генераторов в базе данных (128 при размере страницы 1К). Даже и без бага этот способ приведет к постоянному росту числа страниц, выделенных под генераторы.

При помощи генераторов можно также решить проблему с отсутствием временных таблиц в вашей версии сервера (временные таблицы появились в InterBase 7.5LINK). Вы создаете таблицу, например TEMP_TBL, и в качестве первого поля, входящего в первичный ключ, указываете поле типа INTEGER. Пользователь при соединении с сервером получает собственный идентификатор у некоторого общего генератора, и затем использует его при помещении записей в такую "временную" таблицу. В результате, даже если несколько пользователей будут работать с такой таблицей, они никогда не "пересекутся" по значению первого поля первичного ключа "временной" таблицы..

Если вам нужно, чтобы некая процедура выполнялась монопольно, то есть всегда в одном экземпляре (какой-нибудь сложный расчет), то для этого можно использовать генератор следующим образом:
create procedure GENREPORT
as
declare variable i int;
begin
i = GEN_ID(REPGEN, 1); -- проверим, запущена ли процедура
if (i > 1) then -- да, процедура уже работает
begin
i = GEN_ID(REPGEN, -1); -- возвращаем значение обратно
EXCEPTION REPORT_ALREADY_RUNNING;
end
... -- обработка отчета
i = GEN_ID(REPGEN, -1); -- отчет закончен, возвращаем значение обратно
end

Смысл очень прост. Исходное значение генератора = 0. При запуске процедуры она его увеличивает на 1. Если при проверке значения генератора окажется, что он больше 1, то это означает что копия процедуры уже запущена и работает.

Здесь есть скрытая проблема. Если вдруг в момент обработки произойдет ошибка, то без обработки исключений последняя строка в процедуре, возвращающая значение генератора обратно, не выполнится. И в результате второй раз завершившуюся с ошибкой процедуру запустить не удастся. Есть два варианта решения этой проблемы:
  1. обработка ошибок по when any в процедуре, с "возвратом" значения генератора
  2. обработка ошибки из процедуры на клиенте, и явная выдача запроса
select gen_id(repgen, -1) from rdb$database с клиента в случае неуспешного выполнения процедуры.

Разумеется, на всякий случай следует еще проверять, не остался ли генератор в значении < 1 после его увеличения на 1 в начале процедуры. И обязательно предусмотреть обработку ситуации, когда используется 2-ой случай решения проблемы с ошибкой в процедуре, и в результате ошибки потеряно соединение с сервером.

Если вам не нравится использовать генератор для данных целей, то вы можете точно так же использовать функции WaitForAccess и ReleaseAccess из udfdemox. Обработку ошибок при использовании mutex нужно планировать так же, как и для генераторов.

Подпишитесь на новости Firebird в России

Подписаться