Thursday 8 September 2011

The Utility

Having identified the Oracle features of which I will take advantage and the requirements for the various DML operations, I can now get on with the construction of the utility, the final version of which you will find below.
First, though, an important caveat:
This program is not intended to be a "production quality" piece of software. I have tested it for a variety of table structures and it seems to work well enough. You will need, however, to do your own testing and review its output very carefully before using it. I also made no attempt to format the code nicely. Instead, I relied on Toad's automated formatting feature to make the code pretty after I generated the code. You should do the same with the code that you generate.
To give you a sense of the kind of code that I generate, here is the output from my program for the standard Oracle employees tables:
CREATE TABLE copy_employees AS SELECT * FROM employees WHERE 1 = 2
/
ALTER TABLE copy_employees ADD (
changed_on DATE, changed_by VARCHAR2(30), change_type VARCHAR2(6))
/
CREATE OR REPLACE TRIGGER employees_audit
BEFORE INSERT OR UPDATE OR DELETE
ON employees
FOR EACH ROW
DECLARE
l_change_type VARCHAR2 (6);
BEGIN
CASE
WHEN INSERTING
THEN
l_change_type := 'INSERT';
WHEN UPDATING
THEN
l_change_type := 'UPDATE';
WHEN DELETING
THEN
l_change_type := 'DELETE';
END CASE;

IF INSERTING
THEN
INSERT INTO copy_employees
VALUES (:NEW.employee_id, :NEW.first_name, :NEW.last_name
, :NEW.email, :NEW.phone_number, :NEW.hire_date
, :NEW.job_id, :NEW.salary, :NEW.commission_pct
, :NEW.manager_id, :NEW.department_id, SYSDATE
, USER
, l_change_type);
ELSE
INSERT INTO copy_employees
VALUES (:OLD.employee_id, :OLD.first_name, :OLD.last_name
, :OLD.email, :OLD.phone_number, :OLD.hire_date
, :OLD.job_id, :OLD.salary, :OLD.commission_pct
, :OLD.manager_id, :OLD.department_id, SYSDATE
, USER
, l_change_type);
END IF;
END;
/
As you can see, I create an empty copy of the original table, then alter the table to add a few more columns specific to the auditing process.
Then I create the trigger, making sure to use the :NEW values for an insert operation, and :OLD values for the deletes and updates.
Finally, here is the source code for the utility; as you can see, I give you the option to immediately create the objects with dynamic SQL or simply display the statements on your screen. You can, of course, also spool this output to a file, review the file's contents and then run the file when you are confident that the code will do what you want.
I make liberal use of local or nested subprograms (procedures and functions declared in the declaration section of my procedure) to improve the readability of the code. I hope you find it easy to follow.
CREATE OR REPLACE PROCEDURE gen_table_backup (
table_name_in IN VARCHAR2
, copy_prefix_in IN VARCHAR2 DEFAULT NULL
, copy_suffix_in IN VARCHAR2 DEFAULT '_COPY'
, create_now_in IN BOOLEAN DEFAULT FALSE
)
IS
l_column_names DBMS_SQL.varchar2s;

FUNCTION column_names (table_name_in IN VARCHAR2)
RETURN DBMS_SQL.varchar2s
IS
l_names DBMS_SQL.varchar2s;
BEGIN
SELECT column_name
BULK COLLECT INTO l_names
FROM user_tab_columns
WHERE table_name = table_name_in;

RETURN l_names;
END column_names;

FUNCTION copy_table_name (table_name_in IN VARCHAR2)
RETURN VARCHAR2
IS
BEGIN
RETURN copy_prefix_in || table_name_in || copy_suffix_in;
END copy_table_name;

PROCEDURE show_and_create (string_in IN VARCHAR2)
IS
BEGIN
DBMS_OUTPUT.put_line (string_in);
DBMS_OUTPUT.put_line ('/');

IF create_now_in
THEN
EXECUTE IMMEDIATE string_in;
END IF;
END show_and_create;

PROCEDURE make_copy (one_table_name_in IN VARCHAR2)
IS
l_ddl VARCHAR2 (32767);
BEGIN
l_ddl :=
'create table '
|| copy_table_name (one_table_name_in)
|| ' as select * from '
|| one_table_name_in
|| ' where 1 = 2';
show_and_create (l_ddl);
show_and_create
( 'ALTER TABLE '
|| copy_table_name (one_table_name_in)
|| ' ADD (changed_on DATE
, changed_by VARCHAR2(30)
, change_type VARCHAR2(6))'
);
END make_copy;

PROCEDURE make_trigger (one_table_name_in IN VARCHAR2)
IS
l_ddl VARCHAR2 (32767);

FUNCTION insert_statement (change_type_in IN VARCHAR2)
RETURN VARCHAR2
IS
l_insert VARCHAR2 (32767);
BEGIN
l_insert :=
'INSERT INTO '
|| copy_table_name (one_table_name_in)
|| ' VALUES (';

FOR indx IN 1 .. l_column_names.COUNT
LOOP
l_insert :=
l_insert
|| CASE
WHEN indx = 1
THEN NULL
ELSE ','
END
|| ':'
|| CASE
WHEN change_type_in = 'INSERT'
THEN 'NEW'
ELSE 'OLD'
END
|| '.'
|| l_column_names (indx);
END LOOP;

l_insert := l_insert || ', sysdate, user, l_change_type);';
RETURN l_insert;
END insert_statement;
BEGIN
l_ddl :=
'create or replace trigger '
|| one_table_name_in
|| '_audit before insert or update or delete on '
|| one_table_name_in
|| ' for each row declare l_change_type varchar2(6); begin '
|| ' case when inserting then l_change_type := ''insert'';
when updating then l_change_type := ''update'';
when deleting then l_change_type := ''delete'';
end case; if inserting then '
|| insert_statement ('INSERT')
|| ' else '
|| insert_statement ('OTHER')
|| ' end if; end;';
show_and_create (l_ddl);
END make_trigger;
BEGIN
FOR table_rec IN (SELECT table_name
FROM user_tables
WHERE table_name LIKE table_name_in)
LOOP
l_column_names := column_names (table_rec.table_name);
make_copy (table_rec.table_name);
make_trigger (table_rec.table_name);
END LOOP;
END gen_table_backup;
/

No comments:

Post a Comment