Maintaining Data Change History in Oracle

For some software applications, it’s necessary to track changes made to data in order to comply with various regulatory or legal requirements. As an example, let’s say our application has an Oracle database with a table named WIDGET and the application needs to maintain a history of changes made to each record in the WIDGET table. The WIDGET table contains a simple ID key (WIDGET_ID) and a name attribute (WIDGET_NAME). It also has attributes to capture when the row is created/modified and by whom along with an INTERNAL_STATUS field which indicates whether or not the row is active (1) or deleted (0).

CREATE TABLE WIDGET
(
  WIDGET_ID           NUMBER(10),
  WIDGET_NAME         VARCHAR2(150),
  INTERNAL_STATUS     NUMBER(10),
  ROW_ENTERED         DATE,
  ROW_ENTERED_BY      VARCHAR2(15),
  ROW_MODIFIED        DATE,
  ROW_MODIFIED_BY     VARCHAR2(15)
)
/

Next, we’ll create a history table to capture each version of each record. For the purposes of this example, the current active records always reside in the WIDGET table (INTERNAL_STATUS is set to 1). Notice that the table structure closely follows the table structure of the WIDGET table except that a new HISTORY_ID attribute has been added.

CREATE TABLE HISTORY_WIDGET
(
  HISTORY_ID          NUMBER(10),
  WIDGET_ID           NUMBER(10),
  WIDGET_NAME         VARCHAR2(150),
  INTERNAL_STATUS     NUMBER(10),
  ROW_ENTERED         DATE,
  ROW_ENTERED_BY      VARCHAR2(15),
  ROW_MODIFIED        DATE,
  ROW_MODIFIED_BY     VARCHAR2(15)
)
/

We’ll need a unique key for the HISTORY_ID field value in the HISTORY_WIDGET table, so let’s create a new sequence named HISTORY_WIDGET_SEQ.

CREATE SEQUENCE HISTORY_WIDGET_SEQ
  START WITH 1
  MAXVALUE 999999999999999999999999999
  MINVALUE 1
  NOCYCLE
  NOCACHE
  NOORDER
/

Finally, we’ll need to create two triggers responsible for populating the HISTORY_WIDGET table whenever changes are made to the records in the WIDGET table. The trigger POSTDEL_WIDGET is executed after a record is deleted while the trigger POSTUPD_WIDGET is executed after a record is updated. With each data manipulation, the triggers create new records in the history table using the field values from the record that was deleted or updated by using the :OLD reference.

CREATE OR REPLACE TRIGGER POSTDEL_WIDGET
  AFTER DELETE ON WIDGET
  FOR EACH ROW
BEGIN
  INSERT INTO HISTORY_WIDGET
             (HISTORY_ID,
              WIDGET_ID,
              WIDGET_NAME,
              INTERNAL_STATUS,
              ROW_ENTERED,
              ROW_ENTERED_BY,
              ROW_MODIFIED,
              ROW_MODIFIED_BY)
  VALUES     (HISTORY_WIDGET_SEQ.NEXTVAL,
              :OLD.WIDGET_ID,
              :OLD.WIDGET_NAME,
              :OLD.INTERNAL_STATUS,
              :OLD.ROW_ENTERED,
              :OLD.ROW_ENTERED_BY,
              :OLD.ROW_MODIFIED,
              :OLD.ROW_MODIFIED_BY);
END;
/

CREATE OR REPLACE TRIGGER POSTUPD_WIDGET
  AFTER UPDATE ON WIDGET
  FOR EACH ROW
BEGIN
  INSERT INTO HISTORY_WIDGET
             (HISTORY_ID,
              WIDGET_ID,
              WIDGET_NAME,
              INTERNAL_STATUS,
              ROW_ENTERED,
              ROW_ENTERED_BY,
              ROW_MODIFIED,
              ROW_MODIFIED_BY)
  VALUES     (HISTORY_WIDGET_SEQ.NEXTVAL,
              :OLD.WIDGET_ID,
              :OLD.WIDGET_NAME,
              :OLD.INTERNAL_STATUS,
              :OLD.ROW_ENTERED,
              :OLD.ROW_ENTERED_BY,
              :OLD.ROW_MODIFIED,
              :OLD.ROW_MODIFIED_BY);
END;
/

Since this is a very simple way of maintaining data history, it is important to note how the application should manipulate the data. It is the responsibility of the application to populate the ROW_MODIFIED and ROW_MODIFIED_BY attributes when updating records. More importantly, when deleting records, the application should first perform an update on the record to set the INTERNAL_STATUS_ID equal to 0 and to set the ROW_MODIFIED and ROW_MODIFIED_BY attributes. If this crucial step is not performed, then the history will not include a record capturing when a record was deleted and by whom.