Эффективное кодирование на PL/SQLИсточник: oracle Аруп Нанда, член-директор Oracle ACE
Триггеры, которые срабатывают по несколько раз в зависимости от события, возможность указать триггерам одного типа последовательность срабатывания, новое предложение CONTINUE - вот некоторые из новых возможностей, которые упрощают программирование на PL/ SQL. С самого начала PL/SQL был языком, выбранным для программирования в Oracle Database. Через какое-то время стало заметно, что благодаря всё большей функциональности, которая требует меньше кодирования язык развился до степени, достаточной до всесторонней разработки. Oracle Database 11 g делает кодирование на PL/SQL ещё более эффективным для программистов. В этой статье мы рассмотрим несколько примеров, которые позволяют вкратце ознакомиться с новой функциональностью. Составные триггеры Рассмотрим гостиничную базу данных: журналы комнат гостиницы хранятся в таблице BOOKINGS. Нужно также записывать изменения этой таблице для её контроля - что-то наподобие аудита, но с одной особенностью: нужно делать это транзакционно. Триггеры подходят для этого лучше всего. Тем самым нужен небольшой триггер события after-update для строки, который записывает старые и новые значения в таблицу BOOKINGS_HIST, а также того, кто внёс изменение. Пока всё хорошо. Есть, однако, небольшая проблемка. Триггер after-update-row срабатывает для каждой строки, а некоторые записи изменяются в массовом порядке, изменяя сотни строк за одну транзакцию. Отдельные срабатывания триггера after-update-row для каждой из строк и выполнение каждый раз вставки записи в таблицу bookings_hist делают производительность не оптимальной. Лучше было бы сложить вместе эти вставки в таблицу bookings_hist и выполнить их кучей. Это можно осуществить через сложную серию триггеров. Суть в том, что надо поместить значения, предназначенные для таблицы bookings_hist, в коллекцию в триггере на каждую стоку, а затем загрузить данные из коллекции в таблицу bookings_hist с помощью триггера after-update для предложения, который срабатывает только один раз. Так как фактически вставка происходит только один раз, процесс выполняется быстрее, чем вставка каждой отдельной строки. Но это два различных триггера с различными кодами. Есть только один способ передать переменную с коллекцией из одного триггера в другой - создать пакет с переменной-коллекцией, такой как массив или PL/SQL-таблица, в спецификации пакета, заполнить её в строчном триггере after-update и считать в триггере after на предложение - а это непростая задача. Не проще было бы вместо этого поместить все триггеры в одном коде? В Oracle Database 11 g можно использовать compound(составные) триггеры . Составные триггеры представляют собой четыре различных триггера, объявленных как один. Например, составной UPDATE-триггер имеет before для предложения, before для строки, after для предложения и after для строки, одновременно присутствующие в одном составном триггере. Вот часть кода, описывающая, как можно передать переменные будто бы внутри одного монолитного PL/SQL-кода. Рассмотрим пример. Номера строк добавлены, чтобы было проще его объяснять. 1 create or replace trigger tr_bookings_track 2 for update of booking_dt 3 on bookings 4 compound trigger 5 type ty_bookings_hist is table of bookings_hist%rowtype 6 index by pls_integer; 7 coll_bookings_hist ty_bookings_hist; 8 ctr pls_integer := 0; 9 before statement is 10 begin 11 dbms_output.put_line('In before statement'); 12 end before statement; 13 before each row is 14 begin 15 dbms_output.put_line('In before each row'); 16 end before each row; 17 after each row is 18 begin 19 ctr := ctr + 1; 20 dbms_output.put_line('In after each row. booking_id='//:new.booking_id); 21 coll_bookings_hist(ctr).booking_id := :new.booking_id; 22 coll_bookings_hist(ctr).mod_dt := sysdate; 23 coll_bookings_hist(ctr).mod_user := user; 24 coll_bookings_hist(ctr).old_booking_dt := :old.booking_dt; 25 coll_bookings_hist(ctr).new_booking_dt := :new.booking_dt; 26 end after each row; 27 after statement is 28 begin 29 dbms_output.put_line('In after statement'); 30 forall counter in 1..coll_bookings_hist.count() 31 insert into bookings_hist 32 values coll_bookings_hist(counter); 33 end after statement; 34 end tr_bookings_track; Чтобы лучше понять работу триггера, выполним демонстрационный update, который изменяет четыре строки. update bookings set booking_dt = sysdate where booking_id between 100 and 103; Вот результат: In before statement In before each row In after each row. booking_id=100 In before each row In after each row. booking_id=101 In before each row In after each row. booking_id=102 In before each row In after each row. booking_id=103 In after statement Заметьте, как выполняется составной триггер. Он имеет четыре секции: Как видите, этот код един, но каждая секция выполняется в разное время. В предыдущем примере я поместил предложения dbms_output в различных местах, чтобы показать, как каждая секция выполняется в этих точках. Я изменил четыре строки с booking_ids 100, 101, 102 и 103, и видно, что триггеры before- и after для предложения сработали каждый по одному разу, а триггеры для строки (before и after) по одному разу на строку. (В предыдущем примере триггеры before для предложения и строки не нужны, но я их написал для иллюстрации функциональности). Если посмотреть в таблицу bookings_hist, то можно увидеть, что в ней теперь четыре записи - одна для каждого booking_id - но эти четыре записи были вставлены кучей в конце предложения, а не при изменении каждой строки: BOOKING_ID MOD_DT MOD_USER OLD_BOOKI NEW_BOOKI ---------- --------- ------------------------------ --------- --------- 100 27-SEP-07 ARUP 28-AUG-07 27-SEP-07 101 27-SEP-07 ARUP 06-AUG-07 27-SEP-07 102 27-SEP-07 ARUP 04-SEP-07 27-SEP-07 103 27-SEP-07 ARUP 15-JUN-07 27-SEP-07 Одна очень полезная возможность составных триггеров состоит в том, что внутренние объекты PL/SQL-кода, такие как переменные, пакеты и т.п., создаются при срабатывании триггера, а в конце работы триггера их состояние очищено. В примере выше видно, что коллекция не инициализировалась и содержимое коллекции не удалялось. Всё это было сделано автоматически без моего вмешательства. Последовательность выполнения триггеров Начиная с Oracle8 появилась возможность описать несколько триггеров одинакового типа на одной таблице - например два строчных after-триггера при вставке в одну таблицу. Тип триггеров определяет порядок выполнения: перед предложением, перед строкой, после предложения и после строки. Однако, если есть два строчных after-триггера, T1 и T2, то какой из них сработает первым? Выполнение триггеров одинакового типа произвольно или по крайне мере не гарантированно следует шаблону. Является ли это проблемой? Давайте рассмотрим пример таблицы PAYMENTS, показанный ниже: Name Null? Type ----------------------------------------- -------- ---------------- PAY_ID NUMBER(10) CREDIT_CARD_NO VARCHAR2(16) AMOUNT NUMBER(13,2) PAY_MODE VARCHAR2(1) RISK_RATING VARCHAR2(6) FOLLOW_UP VARCHAR2(1) Необходимо вычислить рейтинг риска в зависимости от типа платежей и суммы и сохранить его в столбце RISK_RATING. Следующий простой строчный триггер before-update легко справляется с этой задачей: before update on payments for each row begin dbms_output.put_line ('This is tr_pay_risk_rating'); if (:new.amount) < 1000 then :new.risk_rating := 'LOW'; elsif (:new.amount < 10000) then if (:new.pay_mode ='K') then :new.risk_rating := 'MEDIUM'; else :new.risk_rating := 'HIGH'; end if; else :new.risk_rating := 'HIGH'; end if; end; / Теперь кто-нибудь добавляет ещё одно требование: некоторые значения, зависящие от столбцов RISK_RATING, PAY_MODE, и др. должны быть в столбце FOLLOW_UP помечены признаком повышения. Триггер необходимо модифицировать, но лучше не трогать существующий код, а создать новый триггер такого же типа (строчный before-update), как показано ниже. (Я поместил в код dbms_output, чтобы показать, как триггеры будут срабатывать). create or replace trigger tr_pay_follow_up before update on payments for each row begin dbms_output.put_line ('This is tr_pay_follow_up'); if ( (:new.risk_rating = 'HIGH' and :new.pay_mode = 'C') or (:new.risk_rating = 'MEDIUM' and :new.pay_mode = 'K') or (substr(:new.credit_card_no,1,5) = '23456') ) then :new.follow_up := 'Y'; else :new.follow_up := 'N'; end if; end; / Теперь если выполнить обновление таблицы: SQL> get upd_pay 1 update payments set 2 credit_card_no = '1234567890123456', 3 amount = 100000, 4* pay_mode = 'K' SQL> @upd_pay This is tr_pay_follow_up This is tr_pay_risk_rating 1 row updated. SQL> select * from payments; PAY_ID CREDIT_CARD_NO AMOUNT P RISK_R F ---------- ---------------- ---------- - ------ - 1 1234567890123456 100000 C HIGH N Что же случилось? Столбец risk_rating имеет значение HIGH, а столбец pay_mode - значение "C", которые означают, что столбец FOLLOW_UP должен быть "Y", а не "N". Почему? Чтобы ответить на этот вопрос, посмотрите, в каком порядке сработали триггеры: tr_pay_follow_up сработал раньше, чем tr_pay_risk_rating. Последний установил значение столбца как высокий рейтинг. Поэтому, когда первый сработал, он нашёл null (или "N") в столбце risk_rating и поэтому посчитал, что условие удовлетворяется. В этом случае порядок выполнения триггеров очень важен. Если tr_pay_risk_rating не сработает раньше второго, корректные значения не будут установлены, а то, что установится, будет неверной реализацией требований. Единственной простой возможностью была бы замена всей логики на один код и выполнение её в нужном порядке. В Oracle Database 11 g можно в скрипте создания триггера указать выражение, которое устанавливает порядок триггеров. Вот верхняя часть триггера, включающая это выражение: create or replace trigger tr_pay_follow_up before update on payments for each row follows tr_pay_risk_rating begin ... and so on... Это выражение (FOLLOWS <название_триггера>) заставляет триггер срабатывать после указанного триггера. Протестируем это, выполнив скрипт обновления, показанный ранее. SQL> @upd_pay This is tr_pay_risk_rating This is tr_pay_follow_up 1 row updated. SQL> select * from payments; PAY_ID CREDIT_CARD_NO AMOUNT P RISK_R F ---------- ---------------- ---------- - ------ - 1 1234567890123456 100000 C HIGH Y 1 row selected. Как и ожидалось, столбец заполнен корректно. Заметьте также корректный порядок триггеров, который подтверждает, что выражение работает. Когда выполнять нечего, выполняем CONTINUE Наряду со всеми своими возможностями до настоящего времени в PL/SQL была упущена одна важная часть грамматики: как показать, что ничего делать не надо, а надо перейти в конец цикла и продолжить его выполнение. В Oracle Database 11 g в PL/SQL есть новая конструкция CONTINUE, которая используется в цикле. Это предложение перемещает логику в конец цикла, а затем на начало цикла. Вот небольшой пример, который показывает, как управление передаётся на конец цикла, когда счётчик не кратен 10. begin for ctr in 1..100 loop continue when mod(ctr,10) != 0; dbms_output.put_line ('ctr='//ctr); end loop; end; / Результат: ctr=10 ctr=20 ctr=30 ... и так далее ... Другой вариант CONTINUE - это использование Названия Цикла. begin <<OuterLoop>> for outer in 1..10 loop dbms_output.put_line ('-> outer='//outer); for inner in 1..10 loop continue OuterLoop when mod(inner,3) = 0; dbms_output.put_line ('..-> inner='//inner); end loop; end loop; end; / Результат: -> outer=1 ..-> inner=1 ..-> inner=2 -> outer=2 ..-> inner=1 ..-> inner=2 -> outer=3 ..-> inner=1 ..-> inner=2 ... и так далее ... Вместо использования предопределённой конструкции, такой как mod(inner,3), можно использовать функцию, которая выполняет некоторое вычисление. begin <<OuterLoop>> for outer in 1..10 loop dbms_output.put_line ('-> outer='//outer); for inner in 1..10 loop continue OuterLoop when (myfunc = 1); dbms_output.put_line ('..-> inner='//inner); end loop; end loop; end; / Излишне говорить, что эту конструкцию можно использовать только внутри цикла, где она имеет смысл. Если попытаться использовать её снаружи цикла, то появится ошибка компиляции. Прямые последовательности Когда ранее в программе на PL/SQL использовалась последовательность, нужно было использовать конструкцию типа SELECT <последовательность>.NEXTVAL INTO <название_переменной> FROM DUAL вплоть до этого релиза. declare trans_id number(10); begin select myseq.nextval into trans_id from dual; end; Больше не нужно. Вы можете непосредственно присвоить переменной следующее значение последовательности: declare trans_id number(10); begin trans_id := myseq.nextval; end; / Вот что я называю простотой. Предложение "When OTHERS Then" делает что-нибудь Многие PL/SQL-программисты прибегают к опасной практике, оставляя исключение OTHERS проигнорированным, как показано ниже: when OTHERS then NULL; Это говорит примерно о следующем: "Когда возникает ошибка, ничего делать не надо; только проигнорировать или сделать вид, что этого никогда не случится и что это не случится повторно". Если бы только мир был так прост! Эта практика приводит к потенциально ошибочному нестабильному коду. Oracle Database 11 g помогает немного в этом направлении. В нём есть новое замечание PLW-06009, уведомляющее о такой проблеме во время компиляции. Вот пример: create or replace procedure myproc as l_dummy varchar2(1); begin select dummy into l_dummy from dual; exception when OTHERS then null; end; При компиляции процедура компилируется без замечаний, как было в 10 g . Чтобы включить это замечание, необходимо установить параметр сессии. SQL> alter session set plsql_warnings = 'enable:all' 2 / Session altered. SQL> @others1 SP2-0804: Procedure created with compilation warnings SQL> show error Errors for PROCEDURE MYPROC: LINE/COL ERROR -------- ----------------------------------------------------------------- 8/7 PLW-06009: procedure "MYPROC" OTHERS handler does not end in RAISE or RAISE_APPLICATION_ERROR Заметьте, что новое замечание PLW-06009 возникает во время компиляции. Причём это только замечание; компиляция в целом выполнена успешно. Процедуру выполнить можно, но имейте в виду замечание! Отключенные триггеры В широко распространённых производственных системах часто используется окно для редактирования, с помощью которого вносятся изменения. Вот сценарий типичной несостоявшийся "Catch-22" [ упоминание известного романа Джозеф а Хеллер а " Уловка-22 " - прим.ред . ] в таких средах: вы хотите добавить триггер на таблицу и для этого применяете скрипт, однако, когда триггер создан в редакторе, он показывает ошибки компиляции из-за некоторых нежелательных причин, например, пропущенного синонима. Вы хотите создать триггер раньше синонима, но когда он создаётся, то сразу включается, и вы ничего не можете сделать вне этого редактора. Что в этом случае можно предпринять? В Oracle Database 11 g этот сценарий больше не является проблемой. Можно создать триггер изначально отключенным, что позволяет протестировать все ошибки компиляции. А позже при редактировании включить его. Вот как его можно создать: create or replace trigger tr_t after insert on t for each row disable begin insert into t1 (a) values (:new.col_a); end; / Теперь можно проверить его статус: SQL> select status STATUS Даже несмотря на то, что триггер создан отключенным, он должен быть без ошибок. Поэтому, если попытаться создать его с ошибкой (например, используя таблицу "M", которая не существует): 1 create or replace trigger tr_t 2 after insert on t 3 for each row 4 disable 5 begin 6 insert into m (a) values (:new.col_a); 7* end; SQL> / Warning: Trigger created with compilation errors. SQL> show error Errors for TRIGGER TR_T: LINE/COL ERROR -------- ----------------------------------------------------------------- 2/3 PL/SQL: SQL Statement ignored 2/15 PL/SQL: ORA-00942: table or view does not exist Эта особенность очень полезна в процессе контроля изменений. Другое замечательное применение этой возможности - включение триггеров в определённый момент. Например, с помощью триггеров вы создаёте решение для аудита и audit_table ещё не очищена от старых записей. Триггеры можно создать отключенными, а включить их позже, когда таблица будет готова. Именованные параметры функции Рассмотрим простую функцию: create or replace function myfunc ( p_param1 number, p_param2 number ) return number is begin return p_param1 + p_param2; end; / Эта функция делает очень простую операцию, но она хорошо демонстрирует концепцию. Так как в этой функции два параметра, то её можно вызвать, передав параметры, как позиционные значения, а именно: myfunc (1,2) Или как именованные параметры: myfunc ( p_param1 => 1, p_param2 => 2) Однако, в конце концов возникает проблема, если использовать её в select-предложениях. Если в Oracle Database 10 g выполнить следующее предложение: SQL> select myfunc (p_param1=>1,p_param2=>1) from dual; возникнет ошибка: select myfunc (p_param1=>1,p_param2=>1) from dual * ERROR at line 1: ORA-00907: missing right parenthesis В Oracle Database 11 g вы вправе использовать нотацию: SQL> select myfunc (p_param1=>1,p_param2=>1) from dual; MYFUNC(P_PARAM1=>1,P_PARAM2=>1) ------------------------------- 2 1 row selected. ...которая работает правильно. Можно указывать именованную нотацию в конце, а первые параметры должны быть позиционными. Например, следующий вызов, где параметр p_param1 равен 1, будет корректным: select myfunc (1,p_param2=>2) from dual А этот нет (позиционный параметр в конце): SQL> select myfunc (p_param1=>1,2) from dual; select myfunc (p_param1=>1,2) from dual * ERROR at line 1: ORA-06553: PLS-312: a positional parameter association may not follow a named association Взаимозаменяемость динамического курсора и REF-курсора Вы знаете, каким полезным может быть Native Dynamic Cursor, особенно, когда до вызова не знаешь точно, что будет запрашиваться. Динамический PL/SQL можно также использовать через DBMS_SQL. Оба метода имеют свои преимущества. Но что будет, если вы начали разрабатывать программу, в которой используется сначала один метод, а затем необходимо переключиться на другой? В Oracle Database 11 g этот процесс необыкновенно прост. Поддерживаемый пакет DBMS_SQL имеет новую функцию, TO_REFCURSOR, которая конвертирует динамический курсор DBMS_SQL в ref-курсор. Вот пример такой конвертации: 1 create or replace procedure list_trans_by_store 2 ( 3 p_store_id number 4 ) 5 is 6 type num_tab is table of number index by binary_integer; 7 type type_refcur is ref cursor; 8 c_ref_trans_cur type_refcur; 9 c_trans_cur number; 10 trans_id num_tab; 11 trans_amt num_tab; 12 ret integer; 13 l_stmt clob; 14 begin 15 c_trans_cur := dbms_sql.open_cursor; 16 l_stmt := 17 'select trans_id, trans_amt from trans where store_id = :store_id'; 18 dbms_sql.parse(c_trans_cur, l_stmt, dbms_sql.native); 19 dbms_sql.bind_variable(c_trans_cur, 'store_id', p_store_id); 20 ret := dbms_sql.execute(c_trans_cur); 21 c_ref_trans_cur := dbms_sql.to_refcursor(c_trans_cur); 22 fetch c_ref_trans_cur bulk collect into trans_id, trans_amt; 23 for ctr in 1.. trans_id.count loop 24 dbms_output.put_line(trans_id(ctr) // ' ' // trans_amt(ctr)); 25 end loop; 26 close c_ref_trans_cur; 27* end; Предположим, нужно написать общую процедуру, которая не знает списка столбцов в select-выражении во время компиляции. Это тот случай, когда native dynamic SQL становится необходимым. Можно описать для него ref-курсор. Теперь, чтобы стало интереснее, предположим, что вы не знаете всех bind-переменных, для этого случая больше всего подходит dbms_sql. Как выполнить это сложное требование, написав минимум кода? Просто: начните с dbms_sql для bind-переменных, а затем конвертируйте в ref-курсор. Аналогично, чтобы конвертировать Native Dynamic SQL в REF-курсор, необходимо вызвать другую функцию, TO_CURSOR_NUMBER: cur_handle := dbms_sql.to_cursor_number (c_ref_cur); Ref-курсор, определённый в переменной c_ref_cur, должен быть открыт раньше этого вызова. После этого вызова жизнь ref-курсора закончена; манипулировать можно только dbms_sql-курсором. Предположим, что вы знаете bind-переменные во время компиляции, но не знаете списка select; вы можете начать с native dynamic sql, с ref-курсора, а потом заменить его на dbms_sql, чтобы описать и извлечь столбцы из курсора. Заключение Как видите, Oracle Database 11 g содержит несколько улучшений, которые помогают писать код на PL/SQL более эффективно. |