This package supports:
"update dept set deptno = deptno+10"
will
not work whereas the update "update dept set deptno = deptno+1"
will.
The first update will change 10->20, 20->30 and so on. Problem is
that 10->20 is not generating a 'new' primary key. On the other hand,
deptno=deptno+1 does not have this problem since 10->11, 20->21 and
so on.
PROCEDURE update_cascade.on_table Argument Name Type In/Out Default? ------------------------------ ----------------------- ------ -------- P_TABLE_NAME VARCHAR2 IN P_PRESERVE_ROWID BOOLEAN IN DEFAULT P_USE_DBMS_OUTPUT BOOLEAN IN DEFAULT
| Input Name | Default | Usage |
|---|---|---|
| p-table-name | NO DEFAULT | is the name of the parent table |
| p-preserve-rowid | TRUE | affects the generation of the code used to implement the generated packages. If set to TRUE (the default) the rowid of the updated parent row will not change due to the update. If set to FALSE, the rowid of the updated row WILL change BUT the code executes in about 66% of the time. |
| p-use-dbms-output | FALSE | defaults to FALSE which means the update_cascade package will execute (create) the packages/triggers. If set to true, the generated code will be printed using dbms_output.put_line (make sure to set serveroutput on size 100000 before using TRUE if you want to see the code). |
SQL> REM will do the default cascade support on SQL> REM the dept table SQL> REM ROWIDS will be preserved (unchanged). SQL> REM Package will be created in the database SQL> exec update_cascade.on_table( 'dept' ) SQL> REM will do the same as above but will not SQL> REM preserve the rowids of the parent table SQL> REM (the rowids will change). This version SQL> REM runs in about 2/3 of the time of the first SQL> REM one SQL> exec update_cascade.on_table( 'dept', false ) SQL> REM will do the same as above but will not SQL> REM preserve the rowids of the parent table SQL> REM (the rowids will change). This version SQL> REM runs in about 2/3 of the time of the first SQL> REM one. Also, the packages will be written SQL> REM to the SCREEN, not into the database. SQL> exec update_cascade.on_table( 'dept', false, true )The above would generate and compile all of the packages/triggers need to support cascading updates on the dept table to any other table. You would run this any time you
The other mode of calling update_cascade.on_table is as follows:
SQL> set feedback off SQL> spool tmp.sql SQL> exec update_cascade.on_table( p_table_name => 'dept', p_use_dbms_output =>TRUE ) SQL> spool offThe above would generate and print (as opposed to compile) all of the packages/triggers/views needed to support cascading update on the dept table to any other table. You would use this mode to
The other mode of calling update_cascade.on_table is as follows:
SQL> exec update_cascade.on_table( p_table_name => 'dept', p_preserve_rowid =>FALSE )The above would generate faster versions of the udpate cascade packages. They run in 2/3 of the time of the default version but the rowid's of the updated parent records will change. This makes this version less desirable with FORMS. If you use forms heavily, use the default mode so that rowids are preserved. The triggers to cascade can get away with a lot less work in this mode. The triggers fire half the time they would in the default mode and an update that would normally fire and affect 2x the number of rows is not needed. The generated package code is streamlined as well (less actuall code is generated, procedural code not just updates).
SQL> update dept set deptno=deptno+1;
update dept set deptno=deptno+1
*
ERROR at line 1:
ORA-02292: integrity constraint (UCDEMO.SYS_C005184) violated - child record
found
Then to see the update cascade in action, you would:
SQL> @uc
Table created.
Table altered.
Package created.
Package body created.
SQL> exec update_cascade.on_table('dept');
PL/SQL procedure successfully completed.
SQL>
SQL> select * from dept;
DEPTNO DNAME LOC
---------- -------------- -------------
10 ACCOUNTING NEW YORK
20 RESEARCH DALLAS
30 SALES CHICAGO
40 OPERATIONS BOSTON
SQL> select empno, deptno from emp where deptno = 10;
EMPNO DEPTNO
---------- ----------
7839 10
7782 10
7934 10
SQL> update dept set deptno = deptno+1;
4 rows updated.
SQL> select * from dept;
DEPTNO DNAME LOC
---------- -------------- -------------
11 ACCOUNTING NEW YORK
21 RESEARCH DALLAS
31 SALES CHICAGO
41 OPERATIONS BOSTON
SQL> select empno, deptno from emp where deptno in ( 10, 11 );
EMPNO DEPTNO
---------- ----------
7839 11
7782 11
7934 11
SQL>
As you can see, after the package is installed,
the updates cascade successfully to
the child tables. The effect of this is even more
dramatic when you do it to the
emp table. The MGR column of the EMP table points to
the EMPNO column of the EMP
table. In addition, the EMPNO column of the PROJECTS table points to the EMPNO
column of the EMP table. The following scenario is a good demo of more complex
integrity:
SQL> select empno, mgr from emp;
EMPNO MGR
---------- ----------
7839
7698 7839
7782 7839
7566 7839
7499 7698
7521 7698
7654 7698
7902 7566
7369 7902
7788 7566
7844 7698
7876 7788
7900 7698
7934 7782
14 rows selected.
SQL> update emp set empno = 8000 where empno = 7698;
update emp set empno = 8000 where empno = 7698
*
ERROR at line 1:
ORA-02292: integrity constraint (UCDEMO.SYS_C005186) violated - child record
found
SQL> exec update_cascade.on_table('emp')
PL/SQL procedure successfully completed.
SQL> update emp set empno = 8000 where empno = 7698;
1 row updated.
SQL> select empno, mgr from emp;
EMPNO MGR
---------- ----------
7839
7782 7839
7566 7839
7499 8000
7521 8000
7654 8000
7902 7566
7369 7902
7788 7566
7844 8000
7876 7788
7900 8000
7934 7782
8000 7839
14 rows selected.
CREATE TABLE EMP (EMPNO NUMBER(4) primary key, ENAME VARCHAR2(10), JOB VARCHAR2(9), MGR NUMBER(4) references emp, HIREDATE DATE, SAL NUMBER(7,2), COMM NUMBER(7,2), DEPTNO NUMBER(2) references dept);The MGR column and the DEPTNO column are not indexed by default. So, for example, if one were to issue:
SQL> update emp set empno = empno+1;This would eventually issue:
update emp set mgr = SOME_NEW_VALUE where mgr = SOME_OLD_VALUE;Since the MGR field is not indexed, this update would do a full scan. It would do this full scan once for each row in EMP that was updated. Unindex.sql will generate output such as:
SQL> @unindex.sql STAT TABLE_NAME COLUMNS COLUMNS ---- ------------------------------ -------------------- -------------------- **** EMP MGR ok EMP DEPTNO DEPTNO ok PROJECTS EMPNO EMPNO, PROJ_NO ok T2 A A, B ok T3 A, B A, B, C SQL>The four **** indicate that MGR is a foreign key in the EMP table that is not indexed. It should be (also see chapter 6 in the server application developers guide for other reasons why un-indexed foreign keys are bad).
SQL> @src/update_cascade/generate prompt Update Cascade on table: DEPT execute update_cascade.on_table( 'DEPT' ) prompt Update Cascade on table: EMP execute update_cascade.on_table( 'EMP' ) prompt Update Cascade on table: T1 execute update_cascade.on_table( 'T1' ) prompt Update Cascade on table: T2 execute update_cascade.on_table( 'T2' )
This package uses three triggers to perform it magic.
The following package spec is what would be generated for the typical 'DEPT' table found in the scott schema (when declaritive RI is used). See the annotations in the spec for a description of what each entry means and how it is used. The annotations are not found in the generated code, the generated code is not commented.
This generated code preserves rowids. The code that preserves rowids will be in bold. This code would not be present in the generated package if rowid preservation was disabled.
SQL> exec update_cascade.on_table('dept',true,true);
The following is a typical package specification generated for a table.
The package spec name is always u || TABLE_NAME || p. The package name is
in mixed case (to prevent collisions with other user objects).
create or replace package "uDEPTp" as --Rowcnt is used to collect the number of rows processed by a given update statement. It is reset in the uDEPTp.reset routine in a before update trigger. The 'inTrigger' variable is used to prevent recursive firing of triggers when p_preserve_rowid = TRUE;
rowCnt number default 0; inTrigger boolean default FALSE; --For each element in the primary key, a table type will be declared and then an array of that type will be declared to 1.) hold the before image, 2.) the after image, and 3.) an empty array used to zero out the previous two arrays.
type C1_type is table of "DEPT"."DEPTNO"%type index by binary_integer; -- empty_C1 C1_type; old_C1 C1_type; new_C1 C1_type; -- --Reset is the routine fired by the BEFORE UPDATE trigger that resets the rowcnt variable and empties out the arrays from the previous invocation.
procedure reset; --Do cascade is the work horse routine. It performs the actual cascade when fired from an AFTER UPDATE trigger.
procedure do_cascade; --Add Entry simply increments the rowcnt and collects the before/after images of the primary keys. It also 'undoes' the update to the primary key by accessing the :new and :old variables.
procedure add_entry ( p_old_C1 in "DEPT"."DEPTNO"%type ,p_new_C1 in out "DEPT"."DEPTNO"%type ); -- end "uDEPTp"; /
create or replace package body "uDEPTp" as -- procedure reset is begin --This line is present in all routines when p_preserve_rowids = TRUE. It prevents recursive firing of the triggers.
if ( inTrigger ) then return; end if; -- rowCnt := 0; old_C1 := empty_C1; new_C1 := empty_C1; end reset; -- procedure add_entry ( p_old_C1 in "DEPT"."DEPTNO"%type ,p_new_C1 in out "DEPT"."DEPTNO"%type ) is begin -- if ( inTrigger ) then return; end if; --This code saves the before and after images in pl/sql tables and 'undoes' the primary key update by setting the new columns back to the old columns.
if ( p_old_C1 <> p_new_C1 ) then rowCnt := rowCnt + 1; old_C1( rowCnt ) := p_old_C1; new_C1( rowCnt ) := p_new_C1; p_new_C1 := p_old_C1; end if; end add_entry; -- procedure do_cascade is begin -- if ( inTrigger ) then return; end if; inTrigger := TRUE; --For every row that was updated we will perform the clone, cascade and delete....
for i in 1 .. rowCnt loopThis insert clones the parent row, duping the old values with the new primary key.
insert into DEPT ( "DEPTNO" ,"DNAME","LOC") select new_C1(i) ,"DNAME","LOC" from "DEPT" a where ( "DEPTNO" ) = ( select old_C1(i) from dual ); --This code is generated only when p_preserve_rowids=true and will flip-flop the old and new primary keys, hence preserving the rowid of the original parent.
update "DEPT" set ( "DEPTNO" ) = ( select decode( "DEPTNO", old_c1(i), new_c1(i), old_c1(i) ) from dual ) where ( "DEPTNO" ) = ( select new_C1(i) from dual ) OR ( "DEPTNO" ) = ( select old_C1(i) from dual ); --Do a cascade update to all children tables.
update "EMP" set ( "DEPTNO" ) = ( select new_C1(i) from dual ) where ( "DEPTNO" ) = ( select old_C1(i) from dual ); --Removing the old primary key value.
delete from "DEPT" where ( "DEPTNO" ) = ( select old_C1(i) from dual); end loop; -- inTrigger := FALSE; reset; exception when others then inTrigger := FALSE; reset; raise; end do_cascade; -- end "uDEPTp"; /
create or replace trigger "uc$DEPT_bu"
before update of
"DEPTNO"
on "DEPT"
begin "uc$DEPT_pkg".reset; end;
The next trigger, the for each row trigger,
simply calls add_entry for each changed
row.
create or replace trigger "uc$DEPT_bufer"
before update of
"DEPTNO"
on "DEPT"
for each row
begin
"uc$DEPT_pkg".add_entry(
:old."DEPTNO"
,:new."DEPTNO"
);
end;
The last trigger, calls do_cascade to effect the change
create or replace trigger "uc$DEPT_au"
after update of
"DEPTNO"
on "DEPT"
begin "uc$DEPT_pkg".do_cascade; end;