I'd like to start off this post by thanking Connor McDonald, a member of my Oracle Developer Advocates team.
I'm a traditional sort of Oracle developer. I learned a long time ago to use row-level triggers to keep track of who created and changed a row and when.
So, for example, for the DG_CLASSES table of the Oracle Dev Gym (which is used both for Dev Gym classes and AskTOM Office Hours), I have these two triggers:
TRIGGER dg_classes_bir
BEFORE INSERT
ON dg_classes
FOR EACH ROW
DECLARE
BEGIN
:new.created_on := SYSDATE;
:new.created_by := qdb_config.apex_user;
:new.changed_on := SYSDATE;
:new.changed_by := qdb_config.apex_user;
END dg_classes_bir;
TRIGGER dg_classes_bur
BEFORE UPDATE
ON dg_classes
FOR EACH ROW
DECLARE
BEGIN
:new.changed_on := SYSDATE;
:new.changed_by := qdb_config.apex_user;
END dg_classes_bur;
Clearly, this kind of auditing has some drawbacks, including:
- I have no idea what was changed, just that something was changed.
- Those triggers fire all the time, so if I wanted to, say, do a batch load of rows and preserve the existing created-changed column values, I have to disable the triggers, do the load, re-enable the triggers.
DECLARE
s VARCHAR2 (100) := 'QDB_PROD';
a VARCHAR2 (100) := 'EXECUTE';
TYPE tables_t IS TABLE OF VARCHAR2 (1000);
l_tables tables_t
:= tables_t (
'DG_CLASSES',
'QDB_COMPEV_ANSWERS',
'QDB_COMP_EVENTS',
'DG_STUDENTS',
'QDB_USERS',
'DG_WORKOUTS',
'DG_USER_WORKOUTS');
BEGIN
/* Enable auditing in the Dev Gym schema */
INSERT INTO schema_list VALUES ('QDB_PROD');
FOR indx IN 1 .. tables_in.COUNT
LOOP
/* Creates audit table, package, triggers */
audit_util.generate_audit_support (
p_owner => schema_in,
p_table_name => tables_in (indx),
p_action => action_in);
/* Disable current BUI and BUR triggers. Can delete later. */
EXECUTE IMMEDIATE
'ALTER TRIGGER ' || tables_in (indx) || '_BIR DISABLE';
EXECUTE IMMEDIATE
'ALTER TRIGGER ' || tables_in (indx) || '_BUR DISABLE';
END LOOP;
END;
/
- see the primary key value of the row that was affected. This will make it easier, for example, to filter the above report to show all changes to, say, user 4750.
- view all the data for that row in the specific audit table
- edit the affected row using an existing edit page in the Dev Gym
And I'll need even more than that. To drill down to the edit page for an entity, I need to figure out (a) the name of the primary key column for a given table and the primary key value for a given audit header row.
So let's take a look at the query underlying the report, and then the code I reference in that query.
SELECT aud$tstamp,
aud$id,
table_name,
dg_audit_util.audit_key_from_header (aud$id) key_value,
dml,
descr,
action,
client_id,
HOST,
module,
os_user,
form_page pagenum,
'P' || form_page || '_AUD_ID' pageitem,
edit_page keypage,
'P' || edit_page || '_' || audit_util.key_colname_from_header (aud$id)
keypageitem
FROM (SELECT a.*,
dg_audit_util.form_page (table_name)
form_page,
dg_audit_util.edit_page (table_name)
edit_page
FROM audit_header a)
Here's the package specification:
PACKAGE dg_audit_util
IS
FUNCTION form_page (table_name_in IN VARCHAR2)
RETURN INTEGER;
FUNCTION edit_page (table_name_in IN VARCHAR2)
RETURN INTEGER;
FUNCTION audit_key_from_header (aud$id_in IN NUMBER)
RETURN INTEGER;
FUNCTION key_colname_from_header (aud$id_in IN NUMBER)
RETURN VARCHAR2;
END;
Here's the package body. Actually I don't think the details are all that interesting. In the first two functions I am simply encapsulating the lists of relevant page numbers. The key_colname_from_header "reverse engineers" my naming convention which is that (a) table names include an application prefix, are plural and in most cases that means simply added an "S" to the entity name, (b) primary key column name is table name singular form (entity name) with "_ID" added to it.
So...an example:
Entity = User
Table name = QDB_USERS
Primary key = USER_ID
But then of course there are exceptions like "DG_CLASSES" and "QDB_QUIZZES," which must be handled by special case.
Entity = Class
Table name = DG_CLASSES
Primary key = CLASS_ID
The audit_key_from_header gets the primary key value from the row in the per-entity audit table (e.g., AUD$DG_CLASSES)
PACKAGE BODY dg_audit_util
IS
FUNCTION form_page (table_name_in IN VARCHAR2)
RETURN INTEGER
IS
BEGIN
RETURN CASE table_name_in
WHEN 'DG_CLASSES' THEN 75
WHEN 'QDB_COMPEV_ANSWERS' THEN 84
WHEN 'QDB_USERS' THEN 90
WHEN 'DG_STUDENTS' THEN 92
WHEN 'DG_USER_WORKOUTS' THEN 101
WHEN 'QDB_COMP_EVENTS' THEN 93
ELSE 106
END;
END;
FUNCTION edit_page (table_name_in IN VARCHAR2)
RETURN INTEGER
IS
BEGIN
RETURN CASE table_name_in
WHEN 'DG_CLASSES' THEN 35
WHEN 'QDB_COMPEV_ANSWERS' THEN 3
WHEN 'QDB_USERS' THEN 109
WHEN 'DG_STUDENTS' THEN NULL
WHEN 'DG_USER_WORKOUTS' THEN NULL
WHEN 'QDB_COMP_EVENTS' THEN 54
ELSE 106
END;
END;
FUNCTION key_colname_from_header (aud$id_in IN NUMBER)
RETURN VARCHAR2
IS
l_name VARCHAR2 (1000);
l_col_name VARCHAR2 (1000);
BEGIN
SELECT table_name
INTO l_name
FROM audit_header
WHERE aud$id = aud$id_in;
l_col_name :=
CASE l_name
WHEN 'DG_CLASSES'
THEN
'CLASS_ID'
WHEN 'QDB_RESULT_CHOICES'
THEN
'RESULT_ID'
ELSE
SUBSTR (l_name,
INSTR (l_name, '_') + 1,
LENGTH (l_name) - INSTR (l_name, '_') - 1)
|| '_ID'
END;
RETURN l_col_name;
END;
FUNCTION audit_key_from_header (aud$id_in IN NUMBER)
RETURN INTEGER
IS
l_name VARCHAR2 (1000);
l_key INTEGER;
BEGIN
SELECT table_name
INTO l_name
FROM audit_header
WHERE aud$id = aud$id_in;
EXECUTE IMMEDIATE
'select '
|| key_colname_from_header (aud$id_in)
|| ' from aud$'
|| l_name
|| ' where aud$id = :id'
INTO l_key
USING aud$id_in;
RETURN l_key;
EXCEPTION
WHEN NO_DATA_FOUND
THEN
RETURN NULL;
END;
END;
Comments
Post a Comment