开发中遇到一个情况:前台页面是一个角色对用户的批量授权,其中用户是多选,多选的结果是控件里会填入逗号分隔的用户ID字符串,比如“TOM,JERRY”,最终要分别插入2张一对多的主从表中。
--主表
CREATE TABLE T_MAIN
(GUID VARCHAR2(32) primary key
,GRANTOR VARCHAR2(30)
,ROLEID VARCHAR2(10)
,CREATED DATE
);
--从表
CREATE TABLE T_DETAIL
(GUID VARCHAR2(32)
,GRANTEE VARCHAR2(32)
,ROLEID VARCHAR2(10)
);
--前台展示的的是一个视图:
CREATE OR REPLACE VIEW V_MAIN_DETAIL AS
SELECT A.GUID,A.GRANTOR,A.ROLEID,A.CREATED,TO_CHAR(WM_CONCAT(B.GRANTEE)) GRANTEES
FROM T_MAIN A,T_DETAIL B
WHERE A.GUID=B.GUID
GROUP BY A.GUID,A.GRANTOR,A.ROLEID,A.CREATED;
前台实现的做法是在主表中插入数据,然后对用户多选结果进行循环插入从表。
今天介绍一种后台实现的方式,即前台只需对视图进行增删改查。
大家知道,Oracle里的视图是不能做DML操作的,尽快Oracle做了很多改进,比如对单表视图或一对一键值关联的多表视图可以做增删改。但一对多或者多对多关联的视图还是不支持增删改操作,会报错ORA-01779: cannot modify a column which maps to a non key-preserved table
事实上,Oracle还提供一个变通方式来完成对视图的增删改操作。就是用instead of触发器。
以下对视图进行insert操作的触发器实现。
CREATE OR REPLACE TRIGGER TRI_V_MAIN_DETAIL
INSTEAD OF INSERT
ON V_MAIN_DETAIL
DECLARE
--分解用分隔符分隔的字符串
cursor cur_GRANTEE(v_GRANTEE varchar2,v_deli varchar2) is
select substr(t.ca,instr(t.ca,v_deli,1,c.lv)+1,instr(t.ca,v_deli,1,c.lv+1)-(instr(t.ca,v_deli,1,c.lv)+1)) as GRANTEE
from (select v_deli||v_GRANTEE||v_deli as ca,length(v_deli||v_GRANTEE||v_deli)-nvl(length(replace(v_deli||v_GRANTEE||v_deli,v_deli)),0)-1 as cnt from dual) t,
(select level lv from dual connect by level<=200) c
where c.lv<=t.cnt;
v_guid varchar2(32);
BEGIN
if :new.GRANTEES is null then
raise_application_error(-20000,'没有选择被授权用户!');
else
select sys_guid() into v_guid
from dual;
--插入主表数据1条
insert into T_MAIN(GUID,
GRANTOR,
ROLEID,
CREATED)
values(v_guid,:new.grantor,:new.roleid,sysdate);
--插入从表数据N条
for rec_GRANTEE in cur_GRANTEE(:new.GRANTEES,',') loop
insert into t_detail(guid,
grantee,
roleid)
values(v_guid,rec_grantee.GRANTEE,:new.roleid);
end loop;
end if;
end;
再试一下INSERT操作,对视图插入1条数据,结果在T_MAIN插入1条数据,在T_DETAIL插入了2条数据。
从上面我们大致可以看出来,INSTEAD OF INSERT触发器的原理实际上是捕捉到对视图的INSERT动作,然后用触发器的程序替换该insert操作。
同理,我们可以对视图进行update和delete操作,以下是update的触发器实现。
CREATE OR REPLACE TRIGGER TRI_V_MAIN_DETAIL_UPD
INSTEAD OF UPDATE
ON V_MAIN_DETAIL
DECLARE
--分解用分隔符分隔的字符串
cursor cur_GRANTEE(v_GRANTEE varchar2,v_deli varchar2) is
select substr(t.ca,instr(t.ca,v_deli,1,c.lv)+1,instr(t.ca,v_deli,1,c.lv+1)-(instr(t.ca,v_deli,1,c.lv)+1)) as GRANTEE
from (select v_deli||v_GRANTEE||v_deli as ca,length(v_deli||v_GRANTEE||v_deli)-nvl(length(replace(v_deli||v_GRANTEE||v_deli,v_deli)),0)-1 as cnt from dual) t,
(select level lv from dual connect by level<=200) c
where c.lv<=t.cnt;
BEGIN
--更新主表数据
UPDATE T_MAIN T SET
GRANTOR=:NEW.GRANTOR,ROLEID=:NEW.ROLEID,CREATED=SYSDATE
WHERE GUID=:NEW.GUID;
if :new.GRANTEES!=:OLD.GRANTEES then
--重新插入从表数据N条
delete from t_detail where guid=:new.GUID;
for rec_GRANTEE in cur_GRANTEE(:new.GRANTEES,',') loop
insert into t_detail(guid,
grantee,
roleid)
values(v_guid,rec_grantee.GRANTEE,:new.roleid);
end loop;
end if;
end;
视图delete触发器的实现如下:
CREATE OR REPLACE TRIGGER TRI_V_MAIN_DETAIL_DEL
INSTEAD OF DELETE
ON V_MAIN_DETAIL
DECLARE
BEGIN
--删除从表数据
DELETE FROM T_DETAIL T WHERE GUID=:OLD.GUID;
--删除主表数据
DELETE FROM T_MAIN T WHERE GUID=:OLD.GUID;
end;
尽管INSTEAD OF触发器比较好的实现了视图的增删改操作,
但我还是不提倡经常使用这种方式来处理真实业务,
原因是:业务逻辑被触发器的代码轻易改变了。
比如上面的案例中,假设前台用户选择的是DBA权限,而在触发器中完全可以被篡改成SYSDBA权限,或者执行一堆其他的操作。很容易引起前台人员和后台人员对于业务逻辑实现的混乱。