Показать сообщение отдельно
Старый 24.01.2011, 18:47   #48  
Gustav is offline
Gustav
Moderator
Аватар для Gustav
SAP
Лучший по профессии 2009
 
1,858 / 1152 (42) ++++++++
Регистрация: 24.01.2006
Адрес: Санкт-Петербург
Записей в блоге: 19
Триггер для дыр. Версия 2
Ну, еще раз и… если не красиво, то гораздо более уверенно.

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

Теперь при попытке выполнения шага «новоеЗначение – староеЗначение» большего, нежели размер кэша (25 или 250) проверяется возможность его выполнения в области дыр и при невозможности размещения этого шага в пределах текущей дыры возникает ошибка времени выполнения. При этом параметры попытки сохраняются в специальном реестре отказов – таблице RecIdRefuseLog. Пользователь на клиенте получает сообщение об ошибке и откат транзакции. Далее предполагается, что пользователь сообщит о своей беде администратору, который заглянет в таблицу RecIdRefuseLog, увидит какой шаг пыталась сделать Аксапта, вычислит разность нового и старого значения, после чего «подкрутит» в таблице SystemSequences поле nextVal, принудительно установив его в начало следующей подходящей по размеру шага (разности) дыры, после чего пользователь попытается выполнить свою критичную операцию повторно.

Это сценарий будет происходить только в том случае, если вы решили не избавляться от честных многострочных insert_recordset’ов путем их превращения в построчные одиночные insert’ы, как было описано выше. Если же вы произвели данную адаптацию (что настоятельно рекомендуется при работе с дырами – раз уж сам процесс в данном случае не совсем стандартен, то почему бы не сделать нестандартными и insert_recordset’ы ), то подобных ошибок не предвидится, и процесс генерации RecId будет гладко перетекать из одной дыры в другую. Однако, теперь вы знаете, что механизм защиты от больших шагов работает и гарантирует, что система не уйдет в тихое массовое повторное генерирование уже использованных идентификаторов RecId.

О других ограничениях можно получить представление при ознакомлении с программным текстом триггера (на PL/SQL для Oracle):
Код:
----------------------------------------------------------------
-- Триггер, управляющий генерацией RecId в дырах. Версия 2.
----------------------------------------------------------------
CREATE OR REPLACE TRIGGER SystemSequences_TBU
BEFORE UPDATE
ON SystemSequences REFERENCING NEW AS New OLD AS Old
FOR EACH ROW
WHEN (
SUBSTR(NLS_LOWER(Old.DataAreaId),1,3) = 'ppp' 
      AND Old.Id = -1 
      AND Old.TabId = 0
      )
DECLARE
    axCacheSize NUMBER(10); -- Axapta 3.0 Cache Size
    upAreaStart NUMBER(10); -- начало "верхней области"    
    cntBetween  NUMBER(10);
    cntAbove    NUMBER(10);   
    holesRange  RecIdHoles%ROWTYPE;
    
    -- локальная процедура логирования отвергнутых попыток изменения nextVal
    PROCEDURE Stop_RecId_Generating (ReasonNote_in IN VARCHAR2)
    IS
    PRAGMA AUTONOMOUS_TRANSACTION; -- для того, чтобы можно было сделать локальный фискальный COMMIT при откате самого триггера  
    BEGIN
        INSERT INTO RecIdRefuseLog
            (AttemtDate, OsUserName, OldNextVal, NewNextVal, ReasonNote)
        VALUES 
            (SYSDATE, sys_context('UserEnv','OS_User'), :Old.NextVal, :New.NextVal, ReasonNote_in);
        COMMIT;            

        RAISE_APPLICATION_ERROR(-20111,'RecId Generating Refuse: ' || ReasonNote_in);
    END;
    
BEGIN

    -- админский ввод любого значения nextVal через "пароль" - специальное значение поля TabId (-111 выбрано произвольно)
    -- при этом все последующие проверки не работают
    -- это необходимо, например, когда при работающем уже триггере нужно установить начальное минимальное значение из таблицы дыр  
    IF :New.TabId = -111 THEN
        :New.TabId := :Old.TabId; -- отмена изменения TabId, но nextVal при этом благополучно изменяется
        RETURN; 
    END IF;

    upAreaStart := 1263764777; -- NextVal перед переключением - передвинуть вперед с запасом на 100 тысяч, чтобы не схватили, пока включаем триггер
                               -- а также доиспользовали уже выделенные ранее RecId
    axCacheSize := 25; -- Axapta 3.0 Cache Size -- 25 или 250 - проверить при помощи: print new SystemSequence().getCacheSize(); pause;

    IF :Old.NextVal > 0 AND :New.NextVal < 0 THEN
    
        Stop_RecId_Generating('Причина 1 - алгоритм работает только на монотонно возрастающем участке') ;
    END IF;

    IF :Old.NextVal >= upAreaStart AND :New.NextVal > upAreaStart THEN
        -- если уже в верхней зоне, то никаких ограничений,
        -- это будет обычное успешное авершение ПОСЛЕ прохождения таблицы дыр
        RETURN;
    END IF;

    IF :Old.NextVal < upAreaStart AND :New.NextVal > upAreaStart THEN
        -- наверное, такого не будет в принципе из-за других ограничений
        Stop_RecId_Generating('Причина 2 - попытка перехода через начало верхней области');
    END IF;
    
    -- проверяем находятся ли старое значение и новое значение + дальнейший резерв в одной дыре
    -- если да, то этим мы гарантируем, что следущему клиенту будет достаточно его 25 значений
    -- при использовании insert_recordset текущая разница:New.NextVal- :Old.NextVal может быть больше 25, поэтому мы делаем проверку (как бы еще раз после предыдущего выделения RecId)
    -- если разница точно равна 25, то эта проверка здесь была бы не нужна, так как мы обечпечиваем наличие 25 номеров на предыдущем шаге
    SELECT COUNT(*) INTO cntBetween 
        FROM RecIdHoles h 
        WHERE :Old.NextVal                   BETWEEN h.FromRecId AND h.ToRecId
          AND :New.NextVal + axCacheSize - 1 BETWEEN h.FromRecId AND h.ToRecId;  

    IF cntBetween = 1 THEN
        -- мы полностью внутри одной из строк в таблице дыр (даже если :New.NextVal-:Old.NextVal > axCacheSize)
        -- это будет обычное успешное завершение ПРИ прохождении таблицы дыр
        RETURN;
    END IF;
    
    -- если дошли сюда, то cntBetween = 0, и наши значения не внутри одной дыры

    IF :New.NextVal-:Old.NextVal > axCacheSize THEN
        -- для insert_recordset здесь мы ничего поменять не сможем, поэтому СТОП
        Stop_RecId_Generating('Причина 3 - попытка сделать шаг больше размера кэша (подозревается insert_recordset)');
    END IF;

    -- если дошли сюда, то однозначно будем устанавливать новое значение :New.NextVal
    
    -- количество диапазонов выше текущего значения :New.NextVal
    SELECT COUNT(*) INTO cntAbove 
        FROM RecIdHoles h 
        WHERE h.FromRecId > :New.NextVal
          AND h.ToRecId - h.FromRecId + 1 >= axCacheSize; -- на всякий случай, в принципе это условие гарантируется самой таблицей

    IF cntAbove = 0 THEN
        -- здесь мы уже не в таблице дыр (как минимум новым значением)
        IF :New.NextVal < upAreaStart THEN
            :New.NextVal := upAreaStart;
        END IF;
    
    ELSE
        -- иначе мы все еще в области, охватываемой таблицей дыр
        -- устанавливаем новое значение, равное FromRecId следующего (ближайшего сверху) диапазона
        SELECT * INTO holesRange
            FROM (SELECT * FROM RecIdHoles h 
                  WHERE h.FromRecId > :New.NextVal
                    AND h.ToRecId - h.FromRecId + 1 >= axCacheSize -- на всякий случай, в принципе это условие гарантируется самой таблицей                 
                  ORDER BY h.FromRecId)
            WHERE ROWNUM = 1;
                
        :New.NextVal := holesRange.FromRecId;
    END IF;


    EXCEPTION
        WHEN OTHERS THEN
        -- Consider logging the error and then re-raise
        RAISE;
END SystemSequences_TBU;

----------------------------------------------------------------
-- Операторы создания таблицы дыр (выполнить последовательно)
----------------------------------------------------------------
CREATE TABLE RecIdHoles
(
  FROMRECID  NUMBER(10) NOT NULL,
  TORECID    NUMBER(10) NOT NULL
)

CREATE UNIQUE INDEX RECIDHOLES_PK ON RECIDHOLES
(FROMRECID, TORECID)

ALTER TABLE RecIdHoles ADD (
  CHECK (TORECID-FROMRECID+1>=25), -- здесь или 25, или 250, или иное желаемое обоснованное значение
  CONSTRAINT RECIDHOLES_PK
 PRIMARY KEY
 (FROMRECID, TORECID) )

----------------------------------------------------------------
-- Таблица логирования отказов
----------------------------------------------------------------
CREATE TABLE RecIdRefuseLog
(
  ATTEMTDATE  DATE,
  OSUSERNAME  VARCHAR2(30 BYTE),
  OLDNEXTVAL  NUMBER(10),
  NEWNEXTVAL  NUMBER(10),
  REASONNOTE  VARCHAR2(100 BYTE)
)
Как запустить триггер на работающей системе
  1. Средствами СУБД в схеме Аксапты создать таблицы RecIdHoles и RecIdRefuseLog (см.операторы SQL по их созданию выше – непосредственно после текста самого триггера).
    .
  2. Таблицу RecIdHoles заполнить строчками-дырами, т.е. диапазонами свободных номеров RecId, предварительно полученными любым желаемым способом, например, с использованием средств, описанных выше в этой ветке.
    .
  3. Дальнейшие настроечные действия настоятельно рекомендуется делать в период минимальной активности пользователей - вечером или в выходной день.
    .
  4. Убедиться, что текущее значение nextVal (для выбранной строки, которую будет обслуживать триггер – см. условие WHEN) в таблице SystemSequences больше, чем максимальное значение поля ToRecId таблицы RecIdHoles (т.е. значение поля ToRecId из последней строки при сортировке по возрастанию). Запомните это значение nextVal (назовем его nextVal1).
    .
  5. В тексте триггера в операторе «upAreaStart := ...;» вместо многоточия прописать произвольное значение, равное nextVal1 + тысяч эдак 100 (жадничать не будем; потом этот оператор надо будет подправить еще раз).
    .
  6. В операторе «axCacheSize := ...;» вместо многоточия прописать значение стандартного размера кэша вашей Аксапты. Обычно это 25 или 250. Значение можно уточнить джобом, используя операторы «print new SystemSequence().getCacheSize(); pause;».
    .
  7. Активировать (создать) триггер на таблице SystemSequences. При этом вы пока еще не направляете процесс генерации RecId в область дыр, он все еще продолжается в «верхней области» - т.е. выше «области дыр», определяемой таблицей RecIdHoles. Однако, отсчет уже идёт от нового значения nextVal, присвоенного вами переменной upAreaStart.
    .
  8. Непосредственно перед следующим действием (перед оператором UPDATE) еще раз запомнить текущее значение nextVal (оно могло измениться за время, прошедшее с предыдущего шага, за счет активности клиентских приложений; назовем это значение nextVal2).
    .
  9. В таблице SystemSequences для выбранной строки установить значение поля nextVal равным минимальному значению поля FromRecId таблицы RecIdHoles (т.е. значению поля FromRecId из первой строки при сортировке по возрастанию). Это значение следует ввести с использованием специального значения парольного поля (в данном примере это поле TabId и его значение -111). Смысл такого причудливого действия – отмена возможного срабатывания последующей логики триггера при вводе начального значения без отключения (DISABLE) самого триггера (в качестве эксперимента попробуйте ввести значение nextVal без использования парольного поля). Пример оператора UPDATE для установки начального значения 568:
    Код:
    UPDATE SystemSequences  SET NextVal = 568, TabId = -111 
    WHERE SUBSTR(NLS_LOWER(DataAreaId),1,3) = 'ppp'  AND Id = -1 AND TabId = 0
    После выполнения этого update процесс генерации RecId начинает идти в области дыр.
    .
  10. Еще раз в тексте триггера в операторе «upAreaStart := ...;» вместо многоточия прописать произвольное значение, равное последнему запомненному nextVal2 + пусть те же 100 тысяч), после чего заменить триггер на таблице (пересоздать - при помощи обычного CREATE OR REPLACE TRIGGER).
    .
  11. Необязательный шаг, но если не хочется, чтобы пропадали свободные идентификаторы между nextVal1 и upAreaStart, следует проверить этот диапазон на заполненность - по всем таблицам БД, грубо говоря, тем же алгоритмом, который используется при определении дыр на предварительном этапе. Проверку следует выполнить через некоторое ощутимое время (сутки, неделя, месяц – выбрать экспертной оценкой самостоятельно). Это время необходимо для того, чтобы у ранее запущенных клиентских приложений закончились RecId в кэше. Полученные при этой проверке дополнительные дыры можно добавить в таблицу RecIdHoles.
Всё. На этом разрешите свою работу по этой теме считать законченной.

Последний раз редактировалось Gustav; 24.01.2011 в 19:41.
За это сообщение автора поблагодарили: sukhanchik (10).