17.01.2011, 12:33 | #41 |
Участник
|
Лично я бы не стал вообще использовать триггера, а делал разовые замены. Логика примерно следующая
1. Создается отдельная таблица "дыр" в нумерации. Причем рассматриваются только большие "дыры" с интервалами не менее, чем тысяч этак 100. 2. Ежедневно запускается пакетник, который анализирует сколько номеров осталось до конца "дыры". Если осталось меньше, чем, скажем, недельный расход RecId, то переключаемся на следующую "дыру". Разумеется, тут предварительно следует сделать анализ примерного расхода RecId в день и расхода RecId при "глобальных" операциях вроде закрытия склада. Как мне кажется, по InventTrans это будет наглядно видно. Преимущество данной схемы в том, что вообще ничего и нигде не меняется. Ну, а недостаток в том, что данная схема будет работать только при наличии достаточно больших "дыр" в нумерации. Т.е. таких "дыр", которых хватит хотя бы на неделю работы. |
|
17.01.2011, 17:30 | #42 |
Moderator
|
Кое-что удалось проверить.
Действительно, insert_recordset генерит значения RecId в количестве, равном количеству записей вставки, начиная со значения nextVal, хранящегося в таблице SystemSequences на момент вызова оператора insert_recordset. После выполнения этого оператора новое значение nextVal = предыдущее значение nextVal + кол-во вставленных записей. Всё это происходит только в том случае, если у таблицы, в которую вставляются записи, не перекрыт метод insert. Если же перекрыт, то insert_recordset превращается в серию обычных одиночных insert-ов. И это известный факт из курса программирования в Аксапте. Как показала проверка, стоит нам создать на таблице простейшую версию метода, например, просто используя ту, которая прописывается автоматически: X++: public void insert() { super(); } Поэтому если есть готовность пожертвовать скоростью вставки, приобретя возможность эффективного использования дыр, то можно просто аналогично перекрыть метод insert() на соответствующей таблице. У меня во всем приложении Аксапты нашлось всего 12 мест в коде с оператором insert_recordset. Но это, конечно, выглядит как полумера (хотя лично мне вполне симпатичная) и я продолжаю подумывать о более универсальном подходе. Пока в голову пришло следующее. Использовать для вставок посредством insert_recordset большую непрерывную область за пределами диапазонов, охватываемых таблицей дыр RecId. Тогда каждому вызову insert_recordset будет предшествовать вызов некоего метода, передвигающего nextVal в непрерывную область, а после insert_recordset другой метод будет восстанавливать предыдущее значение nextVal (опять из области дыр). Парными вызовами этих методов (условно назовем их "BEFORE_insert_recordset" и "AFTER_insert_recordset") необходимо будет "окутать" все операторы insert_recordset. |
|
17.01.2011, 18:47 | #43 |
Member
|
А если рухнет транзакция между переводом в большую область и возвратом взад?
А если конкурентные пользователи начнут хаотично домогаться до строчки с последним номером идентификатора? Блокировки тут будут неприятным, но далеко не самым страшным последствием.
__________________
С уважением, glibs® |
|
17.01.2011, 19:07 | #44 |
Участник
|
|
|
18.01.2011, 13:54 | #45 |
Участник
|
У нас во всем приложении было всего 5 вызовов insert_recordset, причем только один из них реально использовался. Его мы заменили на обычный insert.
Хочу поделиться одним по-моему важным замечанием насчет размера кеша. В двухуровневой конфигурации и в трехуровневой с толстым клиентом размер кэша действительно составляет 25 по умолчанию. А вот в трехуровневой с тонким клиентом - 250! Так что при использовании триггера следует рассматривать "дыры" не меньше 250, а не 25, как заявлялось ранее. |
|
18.01.2011, 16:35 | #46 |
Moderator
|
Обнаружил тему SystemSequences - Выделение RecId, которую, наверное, неплохо иметь в виду при чтении текущей темы.
|
|
|
За это сообщение автора поблагодарили: Alenka (1). |
19.01.2011, 13:11 | #47 |
Moderator
|
Товарищ! Бойся инсерт_рекордсетов, в окрестности максимума RecId происходящих!
Axapta 3.0 SP4, СУБД - Oracle 10.
Исследовал поведение генератора RecId в зоне перехода от плюса к минусу, т.е. ...2147483646, 2147483647, -2147483648, -2147483647, -2147483646... Выяснил, что insert_recordset не особо церемонится с этой областью. При одномоментной вставке 694 записей со значения SystemSequences.nextVal = 2147483646 по окончании получил nextVal = -2147482956. Проверка: 2147483647 - 2147483646 + 1 + (-2147482956 - (-2147483648)) = 694. В таблице же, куда вставлялись записи, при этом обнаружились (средствами СУБД) монотонно возрастающие, все положительные (!), значения RecId от 2147483646 до 2147484339. Дальнейшие попытки открыть таблицу в Обозревателе Аксапты или перебрать ее строки при помощи while select успехом не увенчались. Инфолог сообщал об ошибке SQL "ORA-01455: переполнение при преобразовании столбца к целому типу данных". После выше упомянутой адаптации insert_recordset к построчной вставке всё гладенько перевалилось через ...2147483647, -2147483648... и далее побежало в минусе, закончившись на -2147482957. В Обозревателе теперь всё нормально открылось. |
|
|
За это сообщение автора поблагодарили: pitersky (2). |
24.01.2011, 18:47 | #48 |
Moderator
|
Триггер для дыр. Версия 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) )
Последний раз редактировалось Gustav; 24.01.2011 в 19:41. |
|
|
За это сообщение автора поблагодарили: sukhanchik (10). |
22.08.2013, 11:52 | #49 |
Участник
|
Добрый день!
Подскажите пожалуйста, есть ли в Axapta 3.0 возможность переопределить поведение системного класса SystemSequence (по выделению новых recId, работе с внутренним буфером AOS зарезервированных значений recId и тому подобное)? Цель в том, чтобы иметь более гибкий контроль механизма выделения, чтобы был доступен более широкий контекст, нежели это позволяет способ через sql триггер на таблице SYSTEMSEQUENCES. |
|
22.08.2013, 12:04 | #50 |
Участник
|
Цитата:
там зарыто столько скелетов... прежде всего с кэшированием таблиц и работой в кластере АОСов. я уверен, что никакой "более гибкий" не может перекрыть затрат на борьбу со скелетами. Если у вас всегда один АОС, Если у вас никогда не используется кэширование таблиц, Если у вас никогда не будет виртуальных компаний, Если у вас никогда не будет доменов и т.п. То подумать на тему "переопределить" можно. Если хоть что-то и перечисленного есть - дешевле будет перейти на следующую версию Аксапты. |
|
|
За это сообщение автора поблагодарили: abark (1). |
22.08.2013, 13:09 | #51 |
Участник
|
Цитата:
кеширование (если речь идет про свойство CacheLookup у таблиц) очевидно применяется где-то, например в стандартном функционале, и его отключать не хочется виртуальная компания есть (vrt) домены есть Цитата:
Сообщение от mazzy
Не нужно так делать. Ни в коем случае.
там зарыто столько скелетов... прежде всего с кэшированием таблиц и работой в кластере АОСов. я уверен, что никакой "более гибкий" не может перекрыть затрат на борьбу со скелетами. ... Если хоть что-то и перечисленного есть - дешевле будет перейти на следующую версию Аксапты. Ситуация такая что на достаточно большой базе (1,7 Тб, 4.5 лет) приближается нулевое значение recId. Поэтому ищется как раз решение подешевле. Пока выбирается между: 1) дефрагментацией, 2) реализацией механизма заполнения дырок, хотя очевидно также потребуется 3) чистка старых InventTrans (сильно не тривиальная задача), ну или 4) переезд в новую базу с остатками. Пока реализация механизма заполнения дырок видится самой легкой из вышеперечисленного. Вариант перехода на следующую версию Ax пока не рассматривается. Но встает вопрос про некоторую "ассинхронность" выделения блока новых recId между AOS и SQL. Так триггер на SQL отрабатывая еще не знает какой в будущем непрерывный блок идентификаторов попросит у него AOS, и поэтому алогритмически не может точно выбрать самую подходящую дырку. Остается только реализовывать какие-то эмпирические подходы, ну или в случае попыток выделения чрезмерных блоков обрывать такие попытки через reaiseerror в триггере. При этом Ах-код очевидно может свалиться в самых произвольных местах, и тут остается верить что код достаточно покрыт транзакциями, и/или быть готовым к неожиданностям. Вот поэтому хотелось бы как-то в триггере (или в другом месте - переопределении класса SystemSequence?) в момент резервирования очередного непрерывного блока сразу выдавать ему самую подходящую по размерам дырку. |
|
22.08.2013, 13:12 | #52 |
Участник
|
если еще не делали дефрагментарию на акс3.0, то сначала сделайте ее.
|
|