Introduction
In this blog post, we will explore how to create a history table in a database, which is incredibly useful for tracking changes over time. This setup includes creating a sequence for unique identifiers and a trigger to handle the automatic logging of changes.
Why a History Table?
A history table is essential when you need to track changes to data over time. For example, if customer data such as address or phone number changes, a history table allows you to see what the data was like before the changes. This can help in understanding data trends, auditing, and compliance.
Table Structure
We start with a standard table setup. Here, we assume a simple customer table with fields like ID, name, address, and phone number.
SQL
CREATE TABLE TEST
( TEST_UPLOAD BLOB,
TEST_MIME_TYPE VARCHAR2(4000),
TEST_NAME VARCHAR2(4000),
TEST_ID NUMBER,
A NUMBER,
AA NUMBER,
AB NUMBER,
FDAS NUMBER,
AFDSAFDS NUMBER,
TEST_VALID_FROM NUMBER DEFAULT 5 NOT NULL ENABLE,
TEST_VALID_TO NUMBER DEFAULT 4
) ;
Specifications
Next, we define the sequence and trigger specifications to handle automatic ID generation and capturing data changes.
SQL
/*=============================
===== history trigger =====
============================= */
procedure pr_generate_history(
pi_table_name IN apex_t_varchar2
, pi_delete_yn IN varchar2 default 'NO'
, pi_create_yn in varchar2 default 'NO'
, pi_trigger_yn in varchar2 default 'NO'
);
Trigger Body
The body of the trigger will specify what happens when data changes—specifically how the history table captures these changes.
SQL
/*=============================
===== history trigger =====
============================= */
procedure pr_generate_history(
pi_table_name IN apex_t_varchar2
, pi_delete_yn IN varchar2 default 'NO'
, pi_create_yn in varchar2 default 'NO'
, pi_trigger_yn in varchar2 default 'NO'
)
is
-- global
v_count_total_table number := pi_table_name.COUNT;
v_table_name varchar2(4000);
v_table_history_name varchar2(4000);
v_sequenz_name varchar2(4000);
v_trigger_name varchar2(4000);
-- 2. create history table
v_main_table_name varchar2(4000);
v_check_history_table_exists_yn number;
v_check_sequence_exists_yn number;
v_count number;
v_sql varchar2(4000);
v_h_id_count number;
v_h_created_count number;
-- 3. trigger
v_original_table_name varchar2(4000);
v_primary_key_name_trg varchar2(4000);
v_column_name user_tab_columns.column_name%TYPE;
v_column_list VARCHAR2(4000) := '';
v_new_column_list VARCHAR2(4000) := '';
v_primary_key_count NUMBER;
v_primary_key_name VARCHAR2(4000);
begin
-- 1. delete =============================================================
if pi_delete_yn = 'YES' then
-- 1. ========= delete table ======
FOR i IN 1..v_count_total_table LOOP
-- save table name
v_table_name := pi_table_name(i);
v_table_history_name := 'H_'||pi_table_name(i);
BEGIN
-- drop table
EXECUTE IMMEDIATE 'DROP TABLE ' || v_table_history_name;
-- successfully deleted
DBMS_OUTPUT.PUT_LINE('Table is successfully deleted: ' || v_table_history_name);
-- if error
EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('Failed to delete Table: ' || v_table_history_name || '. Error: ' || SQLERRM);
END;
END LOOP;
-- 2. ========= delete seq ======
FOR i IN 1..v_count_total_table LOOP
-- save sequenz name
v_table_history_name := 'H_'||pi_table_name(i);
v_sequenz_name := v_table_history_name||'_SEQ';
BEGIN
-- drop table
EXECUTE IMMEDIATE 'DROP SEQUENCE ' || v_sequenz_name;
-- successfully deleted
DBMS_OUTPUT.PUT_LINE('Sequence is successfully deleted: ' || v_sequenz_name);
-- if error
EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('Failed to delete Sequence: ' || v_sequenz_name || '. Error: ' || SQLERRM);
END;
END LOOP;
-- 3. ========= delete trigger ======
FOR i IN 1..v_count_total_table LOOP
-- save trigger name
v_table_name := pi_table_name(i);
v_table_history_name := 'H_'||pi_table_name(i);
v_trigger_name := v_table_history_name||'_TRG';
BEGIN
-- drop table
EXECUTE IMMEDIATE 'DROP TRIGGER ' || v_trigger_name;
-- successfully deleted
DBMS_OUTPUT.PUT_LINE('trigger is successfully deleted: ' || v_trigger_name);
-- if error
EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('Failed to delete trigger: ' || v_trigger_name || '. Error: ' || SQLERRM);
END;
END LOOP;
end if;
if pi_create_yn = 'YES' then
-- 2.1 create history table =============================================
FOR i IN 1..v_count_total_table LOOP
-- Zuweisung des Tabellennamens aus der Liste
v_table_name := pi_table_name(i);
v_table_history_name := 'H_'||pi_table_name(i);
-- ===== create if not exists ======================================================
SELECT COUNT(*)
INTO v_check_history_table_exists_yn
FROM user_tables
WHERE LOWER(table_name) = LOWER(v_table_history_name);
IF v_check_history_table_exists_yn = 0 THEN
-- Erstellen der Historientabelle, wenn sie nicht existiert
EXECUTE IMMEDIATE 'CREATE TABLE ' || v_table_history_name || ' AS SELECT * FROM ' || v_table_name || ' WHERE 1=0';
-- successfully message
DBMS_OUTPUT.PUT_LINE('History table successfully created: ' || v_table_history_name);
END IF;
END LOOP;
-- 2.2 create Sequence =============================================
FOR i IN 1..v_count_total_table LOOP
-- Zuweisung des Tabellennamens aus der Liste
v_table_name := pi_table_name(i);
v_table_history_name := 'H_'||pi_table_name(i);
v_sequenz_name := v_table_history_name||'_SEQ';
-- ===== create if not exists ======================================================
SELECT COUNT(*)
INTO v_check_sequence_exists_yn
FROM user_sequences
WHERE LOWER(sequence_name) = LOWER(v_sequenz_name);
IF v_check_sequence_exists_yn = 0 THEN
-- Erstellen der Sequenz für die Historientabelle
EXECUTE IMMEDIATE 'CREATE SEQUENCE ' || v_sequenz_name || ' START WITH 1 INCREMENT BY 1';
-- successfully message
DBMS_OUTPUT.PUT_LINE('Sequence successfully created: ' || v_sequenz_name);
END IF;
END LOOP;
-- 2.3 add new columns =============================================
FOR i IN 1..v_count_total_table LOOP
-- Zuweisung des Tabellennamens aus der Liste
v_table_name := pi_table_name(i);
v_table_history_name := 'H_'||pi_table_name(i);
--========= get columns
FOR r IN (SELECT column_name
, data_type
, data_length
FROM user_tab_columns
WHERE LOWER(table_name) = LOWER(v_table_name)
)
LOOP
-- check history table
SELECT COUNT(*)
INTO v_count
FROM user_tab_columns
WHERE LOWER(table_name) = LOWER(v_table_history_name)
AND LOWER(column_name) = LOWER(r.column_name);
IF v_count = 0 THEN
v_sql := 'ALTER TABLE ' || v_table_history_name || ' ADD ' || r.column_name || ' ' || r.data_type;
IF r.data_type IN ('VARCHAR', 'VARCHAR2', 'CHAR') THEN
v_sql := v_sql || '(' || r.data_length || ')';
END IF;
EXECUTE IMMEDIATE v_sql;
DBMS_OUTPUT.PUT_LINE('New column ' || r.column_name || ' successfully added to table ' || v_table_history_name || '.');
END IF;
END LOOP;
END LOOP;
-- 2.4 add h_id and h_created =============================================
FOR i IN 1..v_count_total_table LOOP
-- Zuweisung des Tabellennamens aus der Liste
v_table_name := pi_table_name(i);
v_table_history_name := 'H_'||pi_table_name(i);
--========= put h_id columns and h_created column to history table
SELECT COUNT(*)
INTO v_h_id_count
FROM user_tab_columns
WHERE LOWER(table_name) = LOWER(v_table_history_name) AND LOWER(column_name) = 'h_id'
;
SELECT COUNT(*)
INTO v_h_created_count
FROM user_tab_columns
WHERE LOWER(table_name) = LOWER(v_table_history_name) AND LOWER(column_name) = 'h_created'
;
-- Hinzufügen der Spalten 'h_id' und 'h_created', wenn sie nicht existieren
IF v_h_id_count = 0 OR v_h_created_count = 0 THEN
v_sql := 'ALTER TABLE ' || v_table_history_name || ' ADD (';
IF v_h_id_count = 0 THEN
v_sql := v_sql || 'h_id NUMBER DEFAULT ' || v_table_history_name || '_SEQ.NEXTVAL PRIMARY KEY';
IF v_h_created_count = 0 THEN
v_sql := v_sql || ', ';
END IF;
END IF;
IF v_h_created_count = 0 THEN
v_sql := v_sql || 'h_created TIMESTAMP DEFAULT SYSDATE';
END IF;
v_sql := v_sql || ')';
EXECUTE IMMEDIATE v_sql;
DBMS_OUTPUT.PUT_LINE('Columns h_id and/or h_created added to the table ' || v_table_history_name || '.');
END IF;
END LOOP;
end if;
-- 3. trigger ==================================================
if pi_trigger_yn = 'YES' then
FOR i IN 1..v_count_total_table LOOP
-- Assign the table name from the list
v_table_name := pi_table_name(i);
v_table_history_name := 'H_'||pi_table_name(i);
v_trigger_name := v_table_history_name || '_TRG';
-- Reset the column list for each iteration
v_column_list := '';
v_new_column_list := '';
-- Query column information
FOR r IN (SELECT column_name
FROM user_tab_columns
WHERE table_name = v_table_name
ORDER BY column_id
)
LOOP
IF v_column_list IS NOT NULL THEN
v_column_list := v_column_list || ', ';
v_new_column_list := v_new_column_list || ', ';
END IF;
v_column_list := v_column_list || r.column_name || chr(13);
v_new_column_list := v_new_column_list || ':NEW.' || r.column_name || chr(13);
END LOOP;
-- Check if the history table contains the columns valid_to and valid_from
SELECT COUNT(*)
INTO v_count
FROM user_tab_columns
WHERE LOWER(table_name) = LOWER(v_table_name)
AND SUBSTR(LOWER(column_name), 6) IN ('valid_to', 'valid_from')
;
-- get primary key of table
SELECT MIN(column_name)
INTO v_primary_key_name_trg
FROM user_tab_columns
WHERE LOWER(table_name) = LOWER(v_table_name)
AND lower(column_name) LIKE '%\_id' ESCAPE '\'
AND lower(column_name) != 'h_id'
AND lower(data_type) = 'number'
;
-- Create the trigger if conditions are met
IF v_count = 2 THEN
v_sql := 'CREATE OR REPLACE TRIGGER ' || v_trigger_name ||chr(13)||
'AFTER INSERT OR UPDATE ON ' || v_table_name ||chr(13)||
'FOR EACH ROW ' ||chr(13)||
'DECLARE ' ||chr(13)||
' v_check_get_old_h_id NUMBER; ' ||chr(13)||
' v_get_old_h_id NUMBER; ' ||chr(13)||
'BEGIN ' ||chr(13)|| chr(13)|| chr(13)||
' ' ||chr(13)||
' SELECT COUNT(*) ' ||chr(13)||
' INTO v_check_get_old_h_id ' ||chr(13)||
' FROM dual ' ||chr(13)||
' WHERE EXISTS (SELECT * ' ||chr(13)||
' FROM '||v_table_history_name ||chr(13)||
' WHERE '||v_primary_key_name_trg||' = :new.'||v_primary_key_name_trg ||chr(13)||
' ) ' ||chr(13)||
' ; ' ||chr(13)||
' ' ||chr(13)|| chr(13)||
' IF v_check_get_old_h_id = 1 THEN ' ||chr(13)||chr(13)||
' SELECT MAX(h_id) ' ||chr(13)||
' INTO v_get_old_h_id ' ||chr(13)||
' FROM '||v_table_history_name ||chr(13)||
' WHERE '||v_primary_key_name_trg||' = :new.'||v_primary_key_name_trg ||chr(13)||
' ; ' ||chr(13)||chr(13)||
' UPDATE '||v_table_history_name ||chr(13) ||
' SET '||SUBSTR(v_primary_key_name_trg, 1, 4)||'_valid_to = :new.'||SUBSTR(v_primary_key_name_trg, 1, 4)||'_valid_from - INTERVAL ''1'' SECOND' ||chr(13) ||
' WHERE h_id = v_get_old_h_id ' ||chr(13) ||
' ; ' ||chr(13) ||
' END IF; ' ||chr(13) ||
' ' ||chr(13)||chr(13)||
' INSERT INTO ' || v_table_history_name || ' (' || v_column_list || ') ' ||chr(13)||
' VALUES (' || v_new_column_list || '); ' ||chr(13)||
'END;';
EXECUTE IMMEDIATE v_sql;
DBMS_OUTPUT.PUT_LINE('Trigger successfully created: ' || v_trigger_name);
ELSE
DBMS_OUTPUT.PUT_LINE('Required columns (valid_to, valid_from) not found in ' || v_table_name);
END IF;
END LOOP;
end if;
end pr_generate_history;
Usage
After setting up the history table, sequence, and trigger, you should now be able to track all changes to the main table.
SQL
declare
l_table_list apex_t_varchar2;
begin
l_table_list := apex_t_varchar2(
'H_ARTICLE'
, 'H_CUSTOMER'
, 'H_EMPLOYEE'
, 'H_TEST'
);
SH_UTIL_PKG.pr_generate_history(
pi_table_name => l_table_list
, pi_delete_yn => 'NO'
, pi_create_yn => 'YES'
, pi_trigger_yn => 'YES'
);
end;
Conclusion
With the structure set up as specified, you should now have a functioning history table where the ID is managed by a sequence, and changes are automatically logged by a trigger. This setup ensures that any modifications to your main data table are traceable and that the historical integrity of your data is maintained.
{fullWidth}
0 $type={blogger}:
Kommentar veröffentlichen