Take Advantage of CONTEXT (dbms_session.set_context) in trigger to sync data bidirectionally

假设我有如下两张一模一样的表... 

create table tab1 (id number);

create table tab2 (id number);

我现在想做的事情是这样的,如果在tab1里面增加记录的同时把这些记录也增加到tab2中,反之tab2中增加的记录也要同步到tab1中去。很自然地想到可以通过在两张表上都创建一个trigger, 让Trigger来进行数据的同步工作。

如下所示...

 
create or replace trigger tri_tab1
before
insert on tab1
for each row
begin
insert into tab2(id) values(:new.id);
end;
/

create or replace trigger tri_tab2
before
insert on tab2
for each row
begin
insert into tab1(id) values(:new.id);
end;
/

“大功告成”了, 测试下...

SQL> insert into tab1 values(1);
insert into tab1 values(1)
*
ERROR at line
1:
ORA
-00036: maximum number of recursive SQL levels (50) exceeded
ORA
-06512: at "FRANK.TRI_TAB2", line 2
ORA
-04088: error during execution of trigger 'FRANK.TRI_TAB2'
ORA
-06512: at "FRANK.TRI_TAB1", line 2
ORA
-04088: error during execution of trigger 'FRANK.TRI_TAB1'
ORA
-06512: at "FRANK.TRI_TAB2", line 2
ORA
-04088: error during execution of trigger 'FRANK.TRI_TAB2'
ORA
-06512: at "FRANK.TRI_TAB1", line 2
ORA
-04088: error during execution of trigger 'FRANK.TRI_TAB1'
ORA
-06512: at "FRANK.TRI_TAB2", line 2
ORA
-04088: error during execution of trigger 'FRANK.TRI_TAB2'
ORA
-06512: at "FRANK.TRI_TAB1", line 2
ORA
-04088: error during execution of trigger 'FRANK.TRI_TAB1'
ORA
-06512: at "FRANK.TRI_TAB2", line 2
ORA
-04088: error during execution of trigger 'FRANK.TRI_TAB2'
ORA
-06512: at "FRANK.TRI_TAB1", line 2
ORA
-04088: error during execution of trigger 'FRANK.TRI_TAB1'
ORA
-06512: at "FRANK.TRI_TAB2", line 2
ORA
-04088: error during execution of trigger 'FRANK.TRI_TAB2'
ORA
-06512: at "FRANK.TRI_TAB1", line 2
ORA
-04088:


SQL
> insert into tab2 values(1);
insert into tab2 values(1)
*
ERROR at line
1:
ORA
-00036: maximum number of recursive SQL levels (50) exceeded
ORA
-06512: at "FRANK.TRI_TAB1", line 2
ORA
-04088: error during execution of trigger 'FRANK.TRI_TAB1'
ORA
-06512: at "FRANK.TRI_TAB2", line 2
ORA
-04088: error during execution of trigger 'FRANK.TRI_TAB2'
ORA
-06512: at "FRANK.TRI_TAB1", line 2
ORA
-04088: error during execution of trigger 'FRANK.TRI_TAB1'
ORA
-06512: at "FRANK.TRI_TAB2", line 2
ORA
-04088: error during execution of trigger 'FRANK.TRI_TAB2'
ORA
-06512: at "FRANK.TRI_TAB1", line 2
ORA
-04088: error during execution of trigger 'FRANK.TRI_TAB1'
ORA
-06512: at "FRANK.TRI_TAB2", line 2
ORA
-04088: error during execution of trigger 'FRANK.TRI_TAB2'
ORA
-06512: at "FRANK.TRI_TAB1", line 2
ORA
-04088: error during execution of trigger 'FRANK.TRI_TAB1'
ORA
-06512: at "FRANK.TRI_TAB2", line 2
ORA
-04088: error during execution of trigger 'FRANK.TRI_TAB2'
ORA
-06512: at "FRANK.TRI_TAB1", line 2
ORA
-04088: error during execution of trigger 'FRANK.TRI_TAB1'
ORA
-06512: at "FRANK.TRI_TAB2", line 2
ORA
-04088:

oops! 很不幸,碰到问题了。从错误信息看出来貌似是出现了“死循环”。 其实仔细想想这个是很正常的,我在两张表都创建了往对方表插入数据的trigger, 这个肯定会陷入万劫不复之地的。比如说我向tab1插入一条记录的时候,由于tab1上有个trigger会向表tab2插入数据,但是tab2上也有个trigger,这个时候也会触发,同时向tab1插入数据,这个时候就又绕回来了,于是乎就这么一直“纠缠”下去。好在oracle不笨,检测到这个情况(ORA-00036: maximum number of recursive SQL levels (50) exceeded)就退出了,不然一直累死不成。

OK,问题知道出在什么地方了,那么接下来自然是要想办法来解决问题了。其实我们是希望这两个trigger不是同时执行的,当tri_tab1执行的时候,tri_tab2就不要执行,因为在任一时刻数据流向是“单向”的,我在表tab1中插入数据的时候是希望数据同时插入tab2中,在tab2中插入数据的时候同时将数据同步到tab1中,仅此而已,不能出现说数据在同一时刻是“双向”流动的。

那么解决完问题的思路应该就是我在执行tri_tab1的时候,把tri_tab2给禁掉(disable),在tri_tab1执行完毕的时候再把tri_tab2恢复(enable)过来。反之依然。很容易想到可以通过"alter trigger xxx disable/enable'来办到,但是因为这是条ddl语句,会影响到transaction的完整性。如果不想影响到当前的transaction,当然可以把上面的开关语句(alter...disable/enable)放到一个单独的autonomous_transaction的procedure中执行,但是这个好像不是很好,把事情搞复杂了。

其实好好研究下定义trigger的语法,可以发现row level的Trigger是有个when语句用来判断何时执行trigger的。那么自然而然地想到可以充分运用这个when语句,但是还有个问题是需要在某个地方设置个flag,这样通过读取这个flag来判断要不要执行这个trigger. 当然这个flag也会被Trigger来更改,这样才能起到开关的作用。很显然,如果借助表来存储flag的话,这个同样会遇到transaction完整性的问题。

那么该怎么办呢? 其实Oracle的context就提供了一种存/取“全局”变量的这样一种机制,我们可以把这个flag设置到一个context中,这样可以通过读取/设置context来更改flag的状态。

OK,下面就可以开工了.... 把trigger改成如下形式...

create or replace trigger tri_tab1
before
insert on tab1
for each row
when
(
NVL(sys_context(
'TEST_CONTEXT', 'TRI_TAB1'), 'Y') <> 'N'
)
begin
dbms_session.set_context(
'TRI_TAB2','N');
insert into tab2(id) values(:new.id);
dbms_session.set_context(
'TRI_TAB2','Y');
end;
/

create or replace trigger tri_tab2
before
insert on tab2
for each row
when
(
NVL(sys_context(
'TEST_CONTEXT', 'TRI_TAB2'), 'Y') <> 'N'
)
begin
dbms_session.set_context(
'TRI_TAB1','N');
insert into tab1(id) values(:new.id);
dbms_session.set_context(
'TRI_TAB1','Y');
end;
/

通过增加WHEN语句,读取context ('TEST_CONTEXT') 中设置的值来判断是否运行Trigger,然后再Trigger中设置这个context的值来更改flag。 但是在执行的时候发现如下错误...

 SQL> insert into tab2 values(1);

insert into tab2 values(1)
*
ERROR at line
1:
ORA
-01031: insufficient privileges
ORA
-06512: at "SYS.DBMS_SESSION", line 90
ORA
-06512: at "FRANK.TRI_TAB1", line 2
ORA
-04088: error during execution of trigger 'FRANK.TRI_TAB1'
ORA
-06512: at "FRANK.TRI_TAB2", line 2
ORA
-04088: error during execution of trigger 'FRANK.TRI_TAB2'

 居然说没有权限执行DBMS_SESSION. 但是即使通过sys用户赋予FRANK这个权限,这个错误同样存在。

后来了解到关于context的设置,oracle是有限制的,也就是dbms_session.set_context必须要在一个独立的package中定义,因此需要将上面的代码改成如下形式...

SQL> create context test_context using context_package;

Context created.

SQL
> create package context_package as
2 procedure name_value(n varchar2, v varchar2);
3 end;
4 /

Package created.

SQL
> create package body context_package as
2 procedure name_value(n varchar2, v varchar2) as
3 begin
4 -- can only be called within the package to which it belongs
5 -- If you try to execute DBMS_SESSION.SET_CONTEXT you'll get an error, as shown here:
6 -- ORA-01031: insufficient privileges
7 dbms_session.set_context('TEST_CONTEXT',n,v);
8 end;
9 end;
10 /

Package body created.

SQL
> create or replace trigger tri_tab1
2 before insert on tab1
3 for each row
4 when
5 (
6 NVL(sys_context('TEST_CONTEXT', 'TRI_TAB1'), 'Y') <> 'N'
7 )
8 begin
9 context_package.name_value('TRI_TAB2','N');
10 insert into tab2(id) values(:new.id);
11 context_package.name_value('TRI_TAB2','Y');
12 end;
13 /

Trigger created.

SQL
> create or replace trigger tri_tab2
2 before insert on tab2
3 for each row
4 when
5 (
6 NVL(sys_context('TEST_CONTEXT', 'TRI_TAB2'), 'Y') <> 'N'
7 )
8 begin
9 context_package.name_value('TRI_TAB1','N');
10 insert into tab1(id) values(:new.id);
11 context_package.name_value('TRI_TAB1','Y');
12 end;
13 /

Trigger created.

现在再来测试一下...

SQL> select * from tab1;

no rows selected

SQL
> select * from tab2;

no rows selected

SQL
> insert into tab1 values(1);

1 row created.

SQL
> select * from tab2;

ID
----------
1

SQL
> select * from tab1;

ID
----------
1

SQL
> insert into tab2 values(3);

1 row created.

SQL
> select * from tab1;

ID
----------
1
3

SQL
> insert into tab2 select * from tab1;

2 rows created.

SQL
> select * from tab2;

ID
----------
1
3
1
3

SQL
> select * from tab1;

ID
----------
1
3
1
3

这下终于大功告成了!

原文地址:https://www.cnblogs.com/fangwenyu/p/2031488.html