Oracle级联修改通用过程
Oracle定义外健的时候可以定义级联删除,但是没有级联修改的语法。当然可以用触发器实现。这里有一个通用的级联修改的过程,代码本身并不复杂,而且说明非常详细(英文)。感谢原作者。
A Generic Cascade Update Procedure
by Michael R. Ault
Introduction
Many times we have the situation where a master table may be updated and dependent tables are left without a link back to the master. In this situation a cascade update option would be a nice one to have. Unfortunately Oracle Corp. doesn't provide this capability in a native manner so a PL/SQL option must be developed.
An example of a need for a cascade update would be in the situation where a dependent table could be dependent on one or more tables. For example, there may be two types of customer, one who has bought from us and we have marketing information for and another that is new to us, may buy from us, but may go with another vendor. If there are dependent tables (such as an interaction log that tracks phone calls to and from customers) it would be nice to be able to switch the dependencies from our new customer table to our established customer table.
Enforcing a Cascade Update
What would be needed to enforce a cascade update? One method would be to utilize data dictionary tables and views to backtrack foreign key relations and then apply updates along this path. However, this may be a lengthy process and can be a performance problem. A simpler method would be to implement a table based cascade update. The table would contain the information a procedure would need to update all tables that are dependent upon a main or master table. Therefore, the table would have to contain the master table name, the dependent table(s) and in case we can't duplicate the exact column name across all of the dependent tables, the column to update. The table DDL script in figure 1 meets these requirements. If required, a fourth column indicating an update order could be added and the cursor in the UPDATE_TABLES procedure detailed later altered to do an ordered retrieve of the information.
CREATE TABLE update_tables
(
main_table VARCHAR2(30) NOT NULL,
table_name VARCHAR2(30) NOT NULL,
column_name VARCHAR2(30) NOT NULL,
CONSTRAINT pk_update_tables
PRIMARY KEY (main_table,table_name,column_name)
USING INDEX
TABLESPACE tool_indexes)
STORAGE (INITIAL 100K NEXT 100K PCTINCREASE 0)
TABLESPACE tools
/
-- Column definitions are as follows:
--
-- main_table holds the name of the table that the update
-- cascades from.
--
-- table_name holds the name(s) of the tables to cascade the update
-- into.
--
-- column_name is the name of the column in the target table(s) to
-- update
Figure 1: Example DDL to create a cascade update source table.
The table by itself would be of little use. Since the data in the table is dynamic (i.e. multiple tables and columns that will need to be addressed) we must enable our trigger to be able to dynamically re-assign these values. The easiest way to do this is to create a set of procedures that utilizes the DBMS_SQL Oracle provided package to dynamically re-assign our update variables. Figure 2 shows the commented code for just such a procedure set. The set consists of two procedures, UPDATE_TABLES and UPDATE_COLUMN.
The Cascade Update Procedures
Use of the DBMS_SQL package to dynamically build the table update command on the fly allows the same set of procedures to be used for any set of master-dependent tables that have entries in the source table.
The UPDATE_TABLES procedure accepts the master table name, the old value for the column to be updated and the new value for the column. The procedure uses a standard cursor fetch to retrieve the dependent table names and dependent table column names from the source table shown in figure 1. If desired, the table from figure 1 could be altered to accept an ordering value for each master-dependent set to allow the cascade update to be done in a specific order if required. Using this information and the new and old values for the column from the trigger call, the UPDATE_COLUMN procedure dynamically rebuilds the table update command to update the appropriate tables.
-- First create package body
-- Decided to use package so that all procedures will
-- be in one place and very controllable
-- M. Ault 1/14/97 Rev 1.0
--
CREATE OR REPLACE PACKAGE cascade_update AS
--
-- First package is update_column
-- This package actually does the work
-- using DBMS_SQL to dynamically rebuild the
-- UPDATEs at run time for each table.
--
PROCEDURE update_column(
old_value IN VARCHAR2,
new_value IN VARCHAR2,
table_name IN VARCHAR2,
update_column IN VARCHAR2
);
--
-- Next procedure is update_tables
-- It is the loop control procedure for
-- the trigger and calls update_column
--
PROCEDURE update_tables(
source_table IN VARCHAR2,
old_value IN VARCHAR2,
new_value IN VARCHAR2
);
--
-- End of PACKAGE HEADER
--
END cascade_update;
/
--
-- Now build package body
-- That actually holds the
-- procedures and code
--
CREATE OR REPLACE PACKAGE BODY cascade_update AS
PROCEDURE update_column(
old_value IN VARCHAR2,
new_value IN VARCHAR2,
table_name IN VARCHAR2,
update_column IN VARCHAR2)
AS
--
-- define state variables for dbms_sql procedures
--
cur INTEGER;
rows_processed INTEGER;
--
-- start processing
-- (dbms_output calls are for debugging
-- commented out during normal runtime)
--
BEGIN
-- DBMS_OUTPUT.PUT_LINE('Table name: '||table_name||' Column: '||update_column);
--
-- initialize the dynamic cursor location for
-- the dbms_sql process
--
cur:=DBMS_SQL.OPEN_CURSOR;
--
-- populate the initialized location with the statement to be
-- processed
--
-- DBMS_OUTPUT.PUT_LINE(
-- 'UPDATE '||table_name||' set '||update_column||'='||chr(39)||new_value||chr(39)||chr(10)||
-- ' WHERE '||update_column||'='||chr(39)||old_value||chr(39)||' AND 1=1');
--
dbms_sql.parse(cur,'UPDATE '||table_name||' set '||update_column||'='||chr(39)||new_value||chr(39)||chr(10)||' WHERE '||update_column||'='||chr(39)||old_value||chr(39)||' AND 1=1',dbms_sql.v7);
--
-- execute the dynamically parsed statement
--
rows_processed:=DBMS_SQL.EXECUTE(cur);
--
-- close dynamic cursor to prepare for next table
--
DBMS_SQL.CLOSE_CURSOR(cur);
--
-- END PROCEDURE
--
END update_column;
PROCEDURE update_tables(
source_table IN VARCHAR2,
old_value IN VARCHAR2,
new_value IN VARCHAR2) as
--
-- Create the cursor to read records
-- from bbs_siteid_tables
-- Use * to prohibit missing a column
--
CURSOR get_table_name IS
SELECT * FROM bbs_update_tables WHERE main_table=source_table;
--
-- Define rowtype variable to hold record from
-- bbs_siteid_tables. Use rowtype to allow for
-- future changes.
--
update_rec update_tables%ROWTYPE;
--
-- start processing
--
BEGIN
--
-- open and fetch values with cursor
--
OPEN get_table_name;
FETCH get_table_name INTO update_rec;
--
-- now that cursor status is open and values in
-- variables can begin loop
--
LOOP
--
-- using the notfound status we had to pre-populate
-- record
--
EXIT WHEN get_table_name%NOTFOUND;
--
-- Initiate call to the update_column procedure
--
update_column(old_value, new_value, update_rec.table_name, update_rec.column_name);
--
-- Now get next record from table
--
FETCH get_table_name INTO update_rec;
--
-- processing returns to loop statement
--
END LOOP;
--
-- close cursor and exit
--
CLOSE get_table_name;
--
-- end of procedure
--
END update_tables;
--
-- end of package body
--
END cascade_update;
/
Figure 2: The package containing the procedures for cascade update
The Final Piece, the Trigger
Once the source table and procedures are built, we need to design a trigger to implement against our master tables that automatically fires on update to the target master column. Figure 3 shows an example of this trigger. One thing to notice about the trigger is that it passes the master table name to the UPDATE_TABLES procedure as well as the old and new values for the column being updated. This allows the UPDATE_TABLES procedure to select only the names and columns for the tables which are dependent upon the master table for which the trigger is implemented. This allows multiple master tables to utilize a single source table.
-- The calling trigger has to be of the form:
CREATE OR REPLACE TRIGGER cascade_update_<tabname>
AFTER UPDATE OF <column> ON <tabname>
REFERENCING NEW AS upd OLD AS prev
FOR EACH ROW
BEGIN
cascade_update.update_tables('<tabname>',:prev.<column>,:upd.<column>);
END;
--
-- Note how the table name is passed to the procedure, this must be done.
Figure 3: Example trigger utilizing the cascade update procedures.
Summary
Utilizing the procedures and trigger described in this article a DBA can enforce a cascade update against any set of master-dependent tables without the performance hit of searching the data dictionary for the relationship definitions.
-----------------------------------------------------------------------------------------------
最近软件系统中要删除一条记录,就要关联到同时删除好多张表,他们之间还存在着约束关系.所以考虑到在创建表时加上约束关系,具体如下:
SQL的外键约束可以实现级联删除与级联更新;
ORACLE则只充许级联删除。
SQL级联删除与级联更新使用格式:
CREATE TABLE A001(ID INT PRIMARY KEY,NAME VARCHAR(20))
CREATE TABLE A002(ID INT REFERENCES A001(ID)ON DELETE CASCADE ON UPDATE CASCADE,AGE TINYINT)
ORACLE级联删除使用格式:
CREATE TABLE A001(ID INT PRIMAY KEY,NAME VARCHAR2(20))
CREATE TABLE A002(ID INT REFERENCES A001(ID)ON DELETE CASCADE,AGE NUMBER(2,0))
SQL与ORACLE的外键约束--级联删除
最近软件系统中要删除一条记录,就要关联到同时删除好多张表,他们之间还存在着约束关系.所以考虑到在创建表时加上约束关系,具体如下:
SQL的外键约束可以实现级联删除与级联更新;
ORACLE则只充许级联删除。
SQL级联删除与级联更新使用格式:
CREATE TABLE A001(ID INT PRIMARY KEY,NAME VARCHAR(20))
CREATE TABLE A002(ID INT REFERENCES A001(ID)ON DELETE CASCADE ON UPDATE CASCADE,AGE TINYINT)
ORACLE级联删除使用格式:
CREATE TABLE A001(ID INT PRIMAY KEY,NAME VARCHAR2(20))
CREATE TABLE A002(ID INT REFERENCES A001(ID)ON DELETE CASCADE,AGE NUMBER(2,0))
--------------
CREATE TABLE groups
(
id VARCHAR2(16) CONSTRAINT pk_groupid PRIMARY KEY,
name VARCHAR2(32),
description VARCHAR2(50)
)
TABLESPACE userspace;
CREATE TABLE usringrp
(
group_id VARCHAR2(16) CONSTRAINT fk_uing_grpid
REFERENCES groups(id)
ON DELETE CASCADE,
user_id VARCHAR2(16)
)
TABLESPACE userspace;
---------------
PowerDesigner
参照完整性约束
限制(Restrict)。不允许进行修改或删除操作。若修改或删除主表的主键时,如果子表中存在子记录,系统将产生一个错误提示。这是缺省的参照完整性设置。
置空(Set Null)。如果外键列允许为空,若修改或删除主表的主键时,把子表中参照的外键列设置为空值(NULL)。
置为缺省(Set Default)。如果指定了缺省值,若修改或删除主表的主键时,把子表中参照的外键设置为缺省值(Default)。
级联(Cascade)。把主表中主键修改为一个新的值时,相应修改子表中外键的值;或者删除主表中主键的记录时,要相应删除子表中外键的记录。
----------------------------------------------------------------------------------------------
我平时比较少用触发器,主要是因为程序逻辑不对的时候不容易发现错误,有时数据量大了也可能
产生性能上的问题,但这个东西总有用武之地,在很多场合还是会起到巨大的作用。
这两天就遇到一个问题,有两张表的一个字段需要进行同步更新,也就是A表修改时要把对应的B表的记录
字段修改,反过来B表修改时也要把A表的修改,保持两边数据的一个同步,这个可以在前台很容易的实现,但开发
人员不想修改代码了,就考虑在后台用trigger实现。
功能很简单,但在实现时遇到一个问题,就是A上的DML触发了上面的TRIGGER,然后这个TRIGGER去更新B表,这样
就会触发B表上的触发器,而B表上的TRIGGER又会更新A表,这样就迭代触发,没有结束了,也就是会产生变异表(mutating)
我不知道ORACLE的触发器是否有属性来限制这种情况的发生,但以前做SQL SERVER时知道有种Instaed of的触发器,他表示
当DML启动他后,他将以TRIGGER里的代码来代替这个DML动作,也就是DML不会真正的执行,只会启动INSTEAD TRIGGER,最终
执行的是TRIGGER里面的编码。
查看了Docs,看到ORACLE也支持这个类型的触发器,但这个只能建立到视图上,不能基于表建立,我要的功能肯定是可以
实现的,在这里我把原表进行了rename,引如了两张视图,名字就是以前的表名,这样对于他们前台应用就做了个
透明的切换,然后在两个视图上建立INSTEAD触发器,将任何两个视图上的更新都传播到后面的两个基表,这样不管你更新那个
视图,我都可以捕获到数据,以代码在后面更新,也不存在互相触发,因为触发器修改的对象已经转移到表了,而此时表上是没有
trigger的,呵呵!!!
过程如下
--创建测试表
SQL> create table mytest1(row_num number,row_name varchar2(50));
表被创建
SQL> create table mytest2(row_num number,row_name varchar2(50));
表被创建
--测试数据
SQL> INSERT INTO MYTEST1 VALUES(1,'Fhhh!!!');
1 行 已插入
SQL> INSERT INTO MYTEST2 VALUES(1,'Watch your mouth!!!');
1 行 已插入
SQL> COMMIT;
提交完成
--先在一个表上创建触发器
SQL> CREATE OR REPLACE TRIGGER TRI_TEST1
2 BEFORE UPDATE
3 ON MYTEST1
4 FOR EACH ROW
5 DECLARE
6 lv_new VARCHAR2(20);
7 lv_parent VARCHAR2(20);
8 BEGIN
9 lv_new := :new.row_name;
10 lv_parent := :OLD.row_name;
11 IF lv_new <> lv_parent THEN
12 UPDATE MYTEST2
13 SET ROW_NAME = :NEW.ROW_NAME
14 WHERE ROW_NUM = :NEW.ROW_NUM;
15 END IF;
16 DBMS_OUTPUT.PUT_LINE(lv_new || lv_parent);
17 END;
18 /
触发器被创建
--测试更新
SQL> set serveroutput on
SQL> UPDATE MYTEST1 SET ROW_NAME = 'DO it!!!';
DO it!!! Fhhhh!!!
1 行 已更新
--更新成功
SQL> SELECT * FROM MYTEST2;
ROW_NUM ROW_NAME
---------- --------------------------------------------------
1 DO it!!!
--另外张表创建触发器
SQL> CREATE OR REPLACE TRIGGER TRI_TEST2
2 BEFORE UPDATE
3 ON MYTEST2
4 FOR EACH ROW
5 DECLARE
6 lv_new VARCHAR2(20);
7 lv_parent VARCHAR2(20);
8 BEGIN
9 lv_new := :new.row_name;
10 lv_parent := :OLD.row_name;
11 IF lv_new <> lv_parent THEN
12 UPDATE MYTEST1
13 SET ROW_NAME = :NEW.ROW_NAME
14 WHERE ROW_NUM = :NEW.ROW_NUM;
15 END IF;
16 DBMS_OUTPUT.PUT_LINE(lv_new || lv_parent);
17 END;
18 /
--产生了变异表,更新失败
SQL> update mytest1 set row_name = 'mouthkkkkkoo';
update mytest1 set row_name = 'mouthkkkkkoo'
ORA-04091: table MYTEST1 is mutating, trigger/function may not see it
ORA-06512: at "TRI_TEST2", line 8
ORA-04088: error during execution of trigger 'TRI_TEST2'
ORA-06512: at "TRI_TEST1", line 8
ORA-04088: error during execution of trigger 'TRI_TEST1'
--更新失败
SQL> update mytest2 set row_name = 'mouthkkkkkoo';
update mytest2 set row_name = 'mouthkkkkkoo'
ORA-04091: table MYTEST2 is mutating, trigger/function may not see it
ORA-06512: at "TRI_TEST1", line 8
ORA-04088: error during execution of trigger 'TRI_TEST1'
ORA-06512: at "TRI_TEST2", line 8
ORA-04088: error during execution of trigger 'TRI_TEST2'
--删除触发器
SQL> drop trigger TRI_TEST2;
触发器被删掉
SQL> drop trigger TRI_TEST1;
触发器被删掉
--创建视图
SQL> CREATE VIEW V_TEST1 AS SELECT * FROM MYTEST1;
视图被创建
SQL> CREATE VIEW V_TEST2 AS SELECT * FROM MYTEST2;
视图被创建
--基于视图创建Instead触发器
SQL> CREATE OR REPLACE TRIGGER TRI_TEST1
2 INSTEAD OF UPDATE
3 ON V_TEST1
4 FOR EACH ROW
5 DECLARE
6 lv_new VARCHAR2(20);
7 lv_parent VARCHAR2(20);
8 BEGIN
9 lv_new := :new.row_name;
10 lv_parent := :OLD.row_name;
11 IF lv_new <> lv_parent THEN
12 UPDATE MYTEST2
13 SET ROW_NAME = :NEW.ROW_NAME
14 WHERE ROW_NUM = :NEW.ROW_NUM;
15 UPDATE MYTEST1
16 SET ROW_NAME = :NEW.ROW_NAME
17 WHERE ROW_NUM = :NEW.ROW_NUM;
18 END IF;
19 DBMS_OUTPUT.PUT_LINE(lv_new || lv_parent);
20 END;
21 /
触发器被创建
SQL> CREATE OR REPLACE TRIGGER TRI_TEST2
2 INSTEAD OF UPDATE
3 ON V_TEST2
4 FOR EACH ROW
5 DECLARE
6 lv_new VARCHAR2(20);
7 lv_parent VARCHAR2(20);
8 BEGIN
9 lv_new := :new.row_name;
10 lv_parent := :OLD.row_name;
11 IF lv_new <> lv_parent THEN
12 UPDATE MYTEST2
13 SET ROW_NAME = :NEW.ROW_NAME
14 WHERE ROW_NUM = :NEW.ROW_NUM;
15 UPDATE MYTEST1
16 SET ROW_NAME = :NEW.ROW_NAME
17 WHERE ROW_NUM = :NEW.ROW_NUM;
18 END IF;
19 DBMS_OUTPUT.PUT_LINE(lv_new || lv_parent);
20 END;
21 /
触发器被创建
--功能已经实现
SQL> update v_test1 set row_name = 'I with you!!!';
1 行 已更新
SQL> commit;
提交完成
SQL> select * from v_test2;
ROW_NUM ROW_NAME
---------- --------------------------------------------------
1 I with you!!!
SQL> update v_test2 set row_name = 'Don't me!!!';
1 行 已更新
SQL> commit;
提交完成
SQL> select * from v_test1;
ROW_NUM ROW_NAME
---------- --------------------------------------------------
1 Don't me!!!
SQL>