СТАТЬЯ |
22.09.00
|
Function Based Indexes, by Tomas Kyte
Эта статья впервые была размещена на сайте www.oramag.ru
От редакции Russian Oracle Magazine
Автономные транзакции предоставляют новый метод контролирования транзакций в хранимых процедурах. Автономные транзакции позволяют создавать новые подтранзакции (subtransaction), которые могут сохранять или отменять изменения вне зависимости от родительской транзакции. Мы рассмотрим, разбирая конкретные примеры:
SQL> create table t ( x int );
Table created. SQL>
SQL> select * from t;
SQL> begin
SQL> select * from t;
|
В вышеприведенном примере, мы создали процедуру INSERT_INTO_T. В этой процедуре используется новая прагма AUTONOMOUS_TRANSACTION. Эта директива сообщает базе данных, что данная процедура будет выполняться как новая подтранзакция, независимая от родительской транзакции. Эта процедура просто вставляет запись со значением ‘1’ в таблицу T и сохраняет изменения. Затем мы создаем анонимный PL/SQL-блок, в котором вставляется значение –1 в таблицу T, вызывается хранимая процедура INSERT_INTO_T и rollback - откат изменений. До внедрения автономных транзакций, оператор commit в процедуре INSERT_INTO_T сохранял бы не только ту работу, которую выполнила процедура (вставка ‘1’), но и любую внешнюю работу, выполненную сессией, но еще не сохраненную (вставка ‘-1’ в анонимном блоке). Оператору Rollback нечего было делать, поскольку оператор commit в процедуре сохранил обе вставки. Мы же видим, что в случае с автономными транзакциями это не так. Работа, выполненная в процедуре, помеченной AUTONOMOUS_TRANSACTION, была сохранена, в то время как работа, выполненная вне автономной транзакции, была отменена.
Предыдущие версии Oracle поддерживали внутренние автономные транзакции. Они известны как рекурсивные SQL-операции. Например, при выборе из некэшируемой последовательности, выполняется рекурсивная транзакция для немедленного увеличения последовательности. Это обновление последовательности сразу же сохраняется, и становится видимым для других транзакций, при этом для всей транзакции сохранение еще не выполнялось. Кроме того, если вы откатите транзакцию, то увеличение последовательности останется неизменным, оно не будет откачено вашей транзакцией, поскольку эти изменения уже сохранены. Управление памятью и другие внутренние операции выполняются аналогичным рекурсивным способом.
Теперь, когда мы выяснили, что такое автономные транзакции, рассмотрим причины, по которым их стоит использовать.
Аудит, который нельзя откатить
Раньше разработчики приложений часто задавали вопрос: “Как можно надежно отследить попытку модифицировать информацию?”. Для этого многие пытались (и безуспешно) использовать триггеры. Триггер должен обнаружить обновление, и если пользователь изменяет данные, которые он не имет права менять, триггер должен создавать аудит-запись и прерывать обновление. К сожалению, при откате обновления, откатывается также и аудит-запись, то есть все или ничего, ошибка должна реализоваться или о ней не будет никаких сведений. Теперь, с появлением автономных транзакций, мы сможем надежно зафиксировать в аудит-таблице попытку выполнить операцию, а также откатить саму операцию. И это даст нам возможность сказать конечному пользователю, что он не может изменять эти данные и что у нас есть запись о пытке это сделать.
Вот небольшой пример:
SQL> REM Создадим для своей работы
копию демонстрационной таблицы EMP.
SQL> REM Предоставим всем пользователям право модифицировать эту SQL> REM таблицу. SQL> create table emp as select * from scott.emp; Table created. SQL> grant all on emp to public;
SQL> REM Это аудит-таблица. Мы будем фиксировать
SQL> create or replace trigger emp_trigger
16 -- как иерархия построена,здесь можно использовать exists 17 select count(*) 18 into l_cnt 19 from dual 20 where exists ( select empno 21 from emp 22 where empno = :new.empno 23 start with mgr = 24 (select empno from emp where ename=USER) 25 connect by prior empno = mgr ); 26 27 -- Если exists ничего не возвращает, значит мы пытаемся 30 -- обновить данные о работнике, нам не подчиненного. 28 -- Необходимо зафиксировать попытку и прервать выполнение 29 -- этой операции. Зарплата сотрудника не будет обновлена, 31 -- а у нас останется запись об этой попытке изменения. 32 if ( l_cnt = 0 ) 33 then 34 insert into audit_tab values ( user, sysdate, 35 'Попытка обновления зарплаты ' || 36 :new.ename || '-' || :new.empno); 37 commit; 38 39 raise_application_error( -20001,
42 end if; 43 end; 44 / Trigger created. |
Итак, вот что мы имеем на данный момент: таблицу EMP, данные которой мы хотим защитить, таблицу AUDIT_TAB, в которую мы будем записывать неудачные попытки обновления данных (попытки, которые мы предотвратили), и триггер, который использует автономную транзакцию для выполнения своей работы. Теперь попробуем выполнить некоторые DML операции, используя учетные записи различных пользователей, и посмотрим, что получится:
SQL> show user
USER is "DEMO_AUTONOMOUS" SQL> REM Сначала мы попытаемся обновить запись, используя SQL> REM учетную запись demo. Это нам не удастся, как покажет SQL> REM результат выборки из emp, приведенный ниже, однако, SQL> REM запись о попытке будет присутствовать в аудит-таблице. SQL> select empno, ename, mgr, sal 2 from emp where ename = 'ADAMS'; EMPNO ENAME MGR SAL ---------- ---------- ---------- ---------- 7876 ADAMS 7788 1100SQL> update emp set sal = sal*2 where ename = 'ADAMS'; update emp set sal = sal*2 where ename = 'ADAMS' * ERROR at line 1: ORA-20001: Вы пытаетесь сделать то, что вы не имеете права делать, и мы знаем об этом ORA-06512: at "DEMO_AUTONOMOUS.EMP_TRIGGER", line 36 ORA-04088: error during execution of trigger 'DEMO_AUTONOMOUS.EMP_TRIGGER' SQL> select empno, ename, mgr, sal
EMPNO ENAME MGR SAL ---------- ---------- ---------- ---------- 7876 ADAMS 7788 1100SQL> select * from audit_tab; UNAME DT MSG
|
Поскольку пользователь DEMO_AUTONOMOUS не имеет подчиненных сотрудников в таблице EMP, это обновление завершается неудачно. Выборка (SELECT) из таблицы EMP демонстрирует, что обновление не было произведено, а выборка из таблицы AUDIT_TAB показывает, что нам удалось обнаружить и зафиксировать попытку обновления.
Теперь, рассмотрим пользователя, который может обновлять некоторые данные. Пользователь SCOTT имеет одного работника (ADAMS), который подчиняется ему.
SQL> show user
USER is "SCOTT" SQL> REM Теперь, попробуем сделать то же самое, используя
EMPNO ENAME MGR SAL br> ---------- ---------- ---------- ---------- 7876 ADAMS 7788 1100SQL> update demo_autonomous.emp set sal = sal*2 where ename = 'ADAMS'; 1 row updated. SQL> select empno, ename, mgr, sal
EMPNO ENAME MGR SAL ---------- ---------- ---------- ---------- 7876 ADAMS 7788 2200SQL> REM Попробуем теперь обновить запись, которую мы не имеем SQL> REM права обновлять (нашу собственную зарплату), и тут мы SQL> REM будем схвачены. SQL> select empno, ename, mgr, sal 2 from demo_autonomous.emp where ename = 'SCOTT'; EMPNO ENAME MGR SAL ---------- ---------- ---------- ---------- 7788 SCOTT 7566 3000SQL> update demo_autonomous.emp set sal = sal*2 where ename = 'SCOTT'; update demo_autonomous.emp set sal = sal*2 where ename = 'SCOTT' * ERROR at line 1: ORA-20001: Вы пытаетесь сделать то, что вы не имеете права делать, и мы знаем об этом ORA-06512: at "DEMO_AUTONOMOUS.EMP_TRIGGER", line 36 ORA-04088: error during execution of trigger 'DEMO_AUTONOMOUS.EMP_TRIGGER' SQL> select empno, ename, mgr, sal
EMPNO ENAME MGR SAL ---------- ---------- ---------- ---------- 7788 SCOTT 7566 3000SQL> connect demo_autonomous/demo_autonomous Connected. SQL> select * from audit_tab; UNAME DT MSG
|
Итак, здесь показано, что SCOTT может обновлять некоторые данные, но вновь, при попытке обновления данных, которые он не имеет права обновлять, SCOTT был схвачен.
Раньше нужно было использовать пакет DBMS_JOB, чтобы спалировать выполнение DDL-предложений после фиксации (commit) транзакции. Этот способ доступен и сейчас, и во многих случаях он является по-прежнему правильным решением. Привлекательной стороной использования DBMS_JOB для планирования выполнения DDL-предложений является то, что это позволяет включить DDL-предложения в транзакцию. Если триггер ставит работу (job) в очередь на выполнение, а эта работа создает пользователя, то при откате родительской транзакции, работа, создающая пользователя, будет также отменена. Ни записей в вашей таблице пользователей, ни пользователя базы данных. Используя же в этом сценарии автономные транзакции, вы создадите пользователя базы данных, но не вставите запись в таблицу жителей. [Прим. редактора: то есть в ту таблицу, вставка в которую была причиной возбуждения автономной транзакции. Автор здесь предупреждает о возможном нарушении целостности данных.] Принимать решение о том, какой именно метод использовать, нужно в зависимости от требований к системе.
Вот небольшой пример, который демонстрирует создание учетной записи пользователя базы данных при вставке пользовательской записи в таблицу "APPLICATION_USERS". Обратите внимание, что создатель этого триггера должен иметь привилегию "CREATE USER", выданную напрямую, а не через роль.
SQL> create user demo_ddl identified by
demo_ddl;
User created. SQL> REM В триггере приведенном ниже, мы хотим предоставить
SQL> grant connect, resource to demo_ddl with admin option;
SQL> REM Кроме того, поскольку мы хотим создавать и удалять
SQL> grant create user to demo_ddl;
SQL> grant drop user to demo_ddl;
SQL> connect demo_ddl/demo_ddl
SQL> REM Создание таблицы для хранения наших пользователей. Мы
SQL> create table application_users ( uname varchar2(30), pw varchar2(30),
SQL> create or replace trigger application_users_aifer
10 execute immediate 11 'grant ' || :new.role_to_grant || 12 ' to ' || :new.uname || 13 ' identified by ' || :new.pw; 14 end; 15 / Trigger created. |
Этот триггер строчного уровня объявлен как автономная транзакция. Это позволяет данному триггеру выполнять DDL-предложения. Мы также используем в этом примере новую возможность задействования динамического sql, появившуюся в PL/SQL, которую мы рассмотрим подробнее в другом разделе. Когда этот триггер сработает, он будет выполнять оператор следующего вида "grant connect, resource to some_username identified by some_password". Этот оператор выполняет команды CREATE USER и GRANT за один проход. Преимуществом этого является то, что если одно из вышеуказанных простых предложений прервется, мы прервем также и родительскую вставку, строка не будет вставлена в таблицу APPLICATION_USERS, и мы сохраним условие целостности. С другой стороны, если бы мы использовали два предложения для создания и предоставления привилегий пользователю, то оператор CREATE USER мог завершиться успешно, а оператор GRANT мог завершиться неудачно. Неудачное завершение оператора GRANT должно вызвать откат вставки, оставляя нас в состоянии, когда учетная запись пользователя создана, привилегии же не предоставлены, и записи в таблице APPLICATION_USERS не существует. Имейте в виду, что многострочная вставка в таблицу APPLICATION_USERS может поставить нас в такое же затруднительное положение, и в этом заключается одна из проблем, связанных с автономными транзакциями. Это похоже на проблему с последовательностями: откат не отменяет увеличение значения в последовательности. Это делает последовательности чрезвычайно удобными для параллельного выполнения (многие пользователи могут одновременно выбирать из них значения), но делает их непригодными для генерации непрерывных последовательностей чисел (откат транзакции после выборки NEXTVAL из последовательности всегда будет оставлять дырку). Вы, как разработчик, должны осознавать это и разрабатывать свои приложения, принимая это во внимание.
Теперь давайте закончим наше приложение:
SQL> create or replace trigger application_users_adfer
2 after delete on application_users 3 for each row 4 declare 5 -- эта прагма позволит нашем триггеру выполнять DDL 6 pragma autonomous_transaction; 7 begin 8 execute immediate 'drop user ' || :old.uname; 9 end; 10 / Trigger created. SQL> REM Проверим, вставив пользователя, которого хотим создать
SQL> REM Для проверки сделанного посмотрим, существует ли новая
SQL> select * from all_users where username = 'NEWUSER';<> USERNAME USER_ID CREATED ------------------------------ ---------- --------- NEWUSER 414 03-JUN-99SQL> connect newuser/newpw Connected. SQL> select * from session_roles; ROLE
SQL> REM Выше показано, что пользователь с указанным паролем
SQL> connect demo_ddl/demo_ddl
SQL> delete from application_users;
SQL> commit;
SQL> select * from all_users where username = 'NEWUSER';
|
Итак, вот он – триггер, способный создавать и удалять пользователей
при вставке и удалении из таблицы базы данных.
SQL> REM создание таблицы демонстрационных
(demo) данных
SQL> create table emp as select * from scott.emp; Table created. SQL> REM Мы будем использовать временную таблицу для хранения
SQL> create global temporary table hierarchy
|
Таблица EMP, созданная выше, содержит данные нашего приложения. Мы напишем к ней запросы, чтобы получить иерархию сотрудников. Временная таблица HIERARCHY действительно является временной таблицей. Способ, которым мы ее определили - "on commit preserve rows", позволяет нашей сессии (и всем транзакциям в этой сессии) увидеть данные сессии, записанные в эту таблицу. Эта таблица будет выглядеть пустой для всех других сессий, до тех пор, пока они не запишут в нее свои данные. При построении иерархии таблицы EMP, с помощью процедуры, описанной ниже, мы запишем построенную иерархию во временную таблицу.
Итак, перейдем к коду, который будет строить иерархию для заданного отдела. Мы должны иметь возможность вызывать эту функцию из SQL, а функция будет вставлять данные во временную таблицу. Раньше это невозможно было сделать. Если процедура изменяла состояние базы данных (выполняла вставку, обновление, удаление), она не могла быть вызвана из SQL. Прагма autonomous_transaction позволяет преодолеть это.
SQL> REM Наша функция заполняет временную
таблицу.
SQL> REM Эта функция принимает на входе номер отдела. Мы начнем с SQL> REM менеджеров данного отдела (то есть сотрудников, управляющих SQL> REM кем-либо в этого отделе). Мы сможем также поддерживать SQL> REM возможность сортировки. SQL> REM Результат этой процедуры похож на запрос с connect by, но он SQL> REM позволяет упорядочивать данные на любом уровне и подуровне, SQL> REM что невозможно сделать, используя connect by. SQL> REM Эта процедура *похожа* на запрос: SQL> REM select * from emp SQL> REM start with empno = :x SQL> REM connect by prior mgr = empno SQL> REM (order by something) SQL> REM Это отличается от случая, когда order by используется для SQL> REM каждого поддерева иерархии, а НЕ для всей иерархии!!! SQL> create or replace
15 l_seq number default 0; 16 l_cur refCur; 17 18 19 -- Эта процедура внутри функции выполняет всю работу. 20 -- Это рекурсивная процедура. Она берет открытый курсор и 21 -- для каждой строки из этого курсора добавляет ее в 22 -- результирующий набор, а затем рекурсивно обрабатывает 23 -- людей, которые работают под руководством этого сотрудника. 24 procedure explode( p_cur in out refcur, 25 p_level in number ) 26 is 27 l_rec emp%rowtype; 28 l_cur refCur; 29 begin 30 loop 31 fetch p_cur into l_rec; 32 exit when p_cur%notfound; 33 34 l_seq := l_seq+1; 35 insert into hierarchy 36 values ( l_seq, p_level, 37 l_rec.empno, l_rec.ename, l_rec.job, l_rec.mgr, 38 l_rec.hiredate, l_rec.sal, l_rec.comm, l_rec.deptno ); 39 40 open l_cur for 'select * 41 from emp 42 where mgr = :x ' || 43 p_order_by 44 USING l_rec.empno; 45 46 explode( l_cur, p_level+1 ); 47 end loop; 48 close p_cur; 49 end; 50 51 begin 52 -- Начнем с очистки нашей временной таблицы. На всякий 53 -- случай, вдруг мы уже запускали такой запрос в этой сессии. 54 55 delete from hierarchy; 56 57 -- Первоначальным набором людей будут те, кто управляет кем- 58 -- либо в интересующем нас отделе 59 60 open l_cur for 'select * 61 from emp 62 where empno in ( select mgr 63 from emp 64 where deptno = :x ) ' || 65 p_order_by 66 USING p_deptno; 67 68 -- Детализировать этот набор (и каждый поднабор) 69 explode( l_cur, 1 ); 70
77 78 -- В случае успешного завершения, возвращаем соответствующее
83 when others then 84 rollback; 85 return sqlerrm; 86 end; 87 / Function created. |
Итак, вот наша процедура. Она начинает работу с запроса "select * from emp where empno in ( select ALL mgr сотрудников в отделе X ) order by <что-нибудь>". Этот запрос открывается в главном блоке и передается в процедуру ‘explode’. Процедура принимает этот запрос и выбирает из него данные. Для каждой строки из этого результирующего набора, процедура строит другой запрос: на этот раз набор всех работников, менеджером которых является текущий сотрудник. Этот запрос передается процедуре, которая делает тоже самое снова (и снова) до тех пор, пока не будет достигнут конец дерева. Рекурсия ‘разворачивается’, и функция завершается. В иерархической таблице создан результирующий набор. Выбрать его теперь легко. Вот несколько примеров, использующих эту технику:
SQL> REM Теперь проверим это. Начнем
с отдела номер 20. Будем
SQL> REM упорядочивать по имени работника (ename) на каждом SQL> REM уровне иерархии SQL> select create_hierarchy( 20, 'order by ename' ) msg from dual; MSG ------------------------------ Ok, результирующий набор создан SQL> REM Выведем теперь результат на экран. Должны быть выведены
SQL> select lpad(' ',lev*2,' ')|| ename ename, hiredate, job, deptno
ENAME HIREDATE JOB DEPTNO -------------------- --------- --------- ---------- FORD 03-DEC-81 ANALYST 20 SMITH 17-DEC-80 CLERK 20 JONES 02-APR-81 MANAGER 20 FORD 03-DEC-81 ANALYST 20 SMITH 17-DEC-80 CLERK 20 SCOTT 09-DEC-82 ANALYST 20 ADAMS 12-JAN-83 CLERK 20 KING 17-NOV-81 PRESIDENT 10 BLAKE 01-MAY-81 MANAGER 30 ALLEN 20-FEB-81 SALESMAN 30 JAMES 03-DEC-81 CLERK 30 MARTIN 28-SEP-81 SALESMAN 30 TURNER 08-SEP-81 SALESMAN 30 WARD 22-FEB-81 SALESMAN 30 CLARK 09-JUN-81 MANAGER 10 MILLER 23-JAN-82 CLERK 10 JONES 02-APR-81 MANAGER 20 FORD 03-DEC-81 ANALYST 20 SMITH 17-DEC-80 CLERK 20 SCOTT 09-DEC-82 ANALYST 20 ADAMS 12-JAN-83 CLERK 20 SCOTT 09-DEC-82 ANALYST 20 ADAMS 12-JAN-83 CLERK 20 23 rows selected. |
SQL> REM Выполним то же самое, упорядочив
на этот раз по дате
SQL> REM приема на работу. SQL> select create_hierarchy( 20, 'order by hiredate' ) msg from dual;
Ok, результирующий набор создан
ENAME HIREDATE_STR JOB DEPTNO ---------------- -------------------- --------- ------ JONES 02-APR-81 MANAGER 20 FORD 03-DEC-81 ANALYST 20 SMITH 17-DEC-80 CLERK 20 SCOTT 09-DEC-82 ANALYST 20 ADAMS 12-JAN-83 CLERK 20 KING 17-NOV-81 PRESIDENT 10 JONES 02-APR-81 MANAGER 20 FORD 03-DEC-81 ANALYST 20 SMITH 17-DEC-80 CLERK 20 SCOTT 09-DEC-82 ANALYST 20 ADAMS 12-JAN-83 CLERK 20 BLAKE 01-MAY-81 MANAGER 30 ALLEN 20-FEB-81 SALESMAN 30 WARD 22-FEB-81 SALESMAN 30 TURNER 08-SEP-81 SALESMAN 30 MARTIN 28-SEP-81 SALESMAN 30 JAMES 03-DEC-81 CLERK 30 CLARK 09-JUN-81 MANAGER 10 MILLER 23-JAN-82 CLERK 10 FORD 03-DEC-81 ANALYST 20 SMITH 17-DEC-80 CLERK 20 SCOTT 09-DEC-82 ANALYST 20 ADAMS 12-JAN-83 CLERK 20 23 rows selected.SQL> REM Проверим, что получится, если задать неверный параметр: SQL> select create_hierarchy( 20, 'order by bogus' ) msg from dual; MSG ------------------------------ ORA-00904: invalid column name |
SQL> declare
2 pragma autonomous_transaction; 3 begin 4 insert into t values ( 1 ); 5 end; 6 / declare * ERROR at line 1: ORA-06519: active autonomous transaction detected and rolled back ORA-06512: at line 4 |
Кроме того, программист должен осознавать, что при использовании автономных транзакций, транзакции могут заблокировать друг друга. Так как автономные транзакции запускаются в отдельном контексте транзакций, они не могут заблокировать никакую строку из тех, что заблокированы родительской транзакцией. Например:
SQL> REM Создадим демонстрационную
таблицу с первичным ключом
SQL> create table t ( x int primary key ); Table created. SQL> REM Вставим туда некоторые данные…
SQL> REM Теперь, в автономной транзакции попытаемся вставить
SQL> declare
declare
|
За | Против |
Разрешает выполнять commit в триггере | Параллельные запросы не могут выполняться в автономных транзакциях. Эти запросы будут выполняться последовательно. |
Позволяет выполнять DML из SELECT | Блокировки могут возникать чаще, так как теперь отдельный пользователь может блокировать сам себя. |
Предоставляет возможность создавать более модульные программы с меньшим побочным эффектом (позволяет избегать ситуаций типа "Эй – вы откатили мою работу!") | Должны быть использованы на верхнем уровне анонимного блока, процедуры или функции. Не могут включаться во вложенные PL/SQL-блоки. |
Позволяет реализовывать аудит, который не может быть отменен | |
Поскольку эта возможность является расширением рекурсивного SQL, она была встроена в ядро в течении длительного времени (а значит хорошо протестирована). |
По этому адресу вы можете найти демонстрационные скрипты, использованные в этой статье. Они удаляют и создают пользователей, вызывают "demo_autonomous", "demo_ddl", and "demo_hierarchy".
За дополнительной информацией обращайтесь в Interface Ltd.
Interface Ltd.Отправить E-Mail http://www.interface.ru |
|
Ваши замечания и предложения отправляйте вебмастеру |