MacLochlainns Weblog

Michael McLaughlin's Technical Blog

Site Admin

Critical Triggers

with one comment

Oracle Critical and Non-critical Triggers

This article demonstrates how you can write critical and non-critical row-level triggers. You may ask yourself, what are critical and non-critical triggers? That’s a great question. A critical trigger stops processing and raises an exception within the scope of an Application Programming Interface (API). An API is typically a series of end-user forms that help you solve business problems. A non-critical trigger either allows users to perform undesired behavior or it automatically fixes undesired behavior by preventing it. Non-critical triggers may log events but they don’t typically raise exceptions to the API.

Next, you’re probably asking yourself if critical and non-critical triggers are important. That’s also a great question. The answer is they’re very important and a key part of any database-centric application software solution.

If you’re new to database triggers, you can read the DML Trigger Basic article on this site to get an introduction. By way of review, you can write database triggers against DDL or DML statements. DML triggers can be either statement-level or row-level triggers.

The difference between a statement-level and row-level trigger is simple. A statement-level trigger runs once for any INSERT, UPDATE, or DELETE statement, which means you can’t inspect the specific rows that a DML statement affects. A row-level trigger runs once for each row affected by an INSERT, UPDATE, or DELETE statement.

Row-level database triggers give us the most granular (fancy word for detailed) view of transactions in your application. They’re also the best suited to logging changes happening with your data. The examples in this article will use DML row-level database triggers.

Business Logic

The article creates some tables for the examples, and the tables use traditional Oracle sequences and triggers. That’s because using sequences and triggers is the closest to how Oracle APEX creates tables. Many readers are familiar with how APEX works. After we create the tables, sequences, and basic automatic numbering database triggers, you will learn how to create non-critical triggers. The last section shows you how to create critical triggers.

It’s helpful to have a basic business problem when you work with so many moving parts. I chose a business problem that should be familiar to most people. The example uses a human resource professional. A human resource professional creates new employees when they join a company. Company policy sometimes dictates the convention for personal names. For example, they may restrict multipart last names. That means when you want to enter a multipart last name; they replace the whitespace with a hyphen.

The example business case requires that all last names must have hyphens. This means that the company disallows multipart last names. While this may seem old fashioned, it’s a simple business process to model, and it lets you see how to work with non-critical and critical database triggers.

So, here are our two use cases:

Non-critical Use Case

A human resource professional may try to enter a multipart last name with whitespace between parts. The entry may be intentional or simply a mistake. Assuming a positive mental attitude, you should assume the human resource profession doesn’t understand the policy. That means our triggers shouldn’t raise an exception when initially entering a value. The insert trigger should only log the attempt to enter non-conforming data. Initial entries, like this, are made through INSERT statements.

Critical Use Case

What the same human resource professional does when they notice that they weren’t able to enter a multipart last name becomes important. A critical trigger becomes necessary when the human resource professional tries to change a hyphenated name into a multipart name. The API uses an UPDATE statement to change an existing value with a new value. There is no use case when the human resource professional accepts the change to a hyphenated name.

The following steps you through how you create a framework for the non-critical and critical triggers. The framework uses three tables.

Framework

The non-critical trigger only uses two of those tables. The non-critical trigger is an INSERT trigger and the critical trigger is an UPDATE trigger. The application_user table will contain information about our authorized users; and the employee table will be the target for our non-critical and critical triggers.

The following creates the application_user table with this statement:

SQL> CREATE TABLE application_user
  2  ( application_user_id    NUMBER
  3  , application_user_name  VARCHAR2(30)  CONSTRAINT application_user_nn1 NOT NULL
  4  , created_by             NUMBER        CONSTRAINT application_user_nn2 NOT NULL
  5  , creation_date          DATE          CONSTRAINT application_user_nn3 NOT NULL
  6  , last_updated_by        NUMBER        CONSTRAINT application_user_nn4 NOT NULL
  7  , last_update_date       DATE          CONSTRAINT application_user_nn5 NOT NULL
  8  , CONSTRAINT application_user_pk  PRIMARY KEY (application_user_id)
  9  , CONSTRAINT application_user_fk1 FOREIGN KEY (created_by)
 10    REFERENCES application_user (application_user_id)
 11  , CONSTRAINT application_user_fk2 FOREIGN KEY (last_updated_by)
 12    REFERENCES application_user (application_user_id));

The application_user_seq supports a surrogate key for the application_user table. You create it with the following statement:

SQL> CREATE OR REPLACE TRIGGER application_user_t1
  2    BEFORE INSERT ON application_user
  3    FOR EACH ROW
  4  BEGIN
  5    /* Check for a empty image_id primary key column value,
  6	  and assign the next sequence value when it is missing. */
  7    IF :NEW.application_user_id IS NULL THEN
  8	 SELECT application_user_seq.NEXTVAL
  9	 INTO	:NEW.application_user_id
 10	 FROM	dual;
 11    END IF;
 12  END;
 13  /

You will need at least one row in the application_user table to test the non-critical and critical triggers. The following insert a single row into the application_user table:

SQL> INSERT INTO application_user
  2  ( application_user_name
  3  , created_by
  4  , creation_date
  5  , last_updated_by
  6  , last_update_date)
  7  VALUES
  8  ('Database Administrator'
  9  , 1
 10  , TRUNC(SYSDATE)
 11  , 1
 12  , TRUNC(SYSDATE));

The next statement creates the employee table:

SQL> CREATE TABLE employee
  2  ( employee_id       NUMBER
  3  , employee_number   VARCHAR2(10)
  4  , first_name        VARCHAR2(20) CONSTRAINT employee_nn1 NOT NULL
  5  , middle_name       VARCHAR2(20)
  6  , last_name         VARCHAR2(20) CONSTRAINT employee_nn2 NOT NULL
  7  , created_by        NUMBER       CONSTRAINT employee_nn3 NOT NULL
  8  , creation_date     DATE         CONSTRAINT employee_nn5 NOT NULL
  9  , last_updated_by   NUMBER       CONSTRAINT employee_nn6 NOT NULL
 10  , last_update_date  DATE         CONSTRAINT employee_nn7 NOT NULL
 11  , CONSTRAINT employee_pk  PRIMARY KEY (employee_id)
 12  , CONSTRAINT employee_fk1 FOREIGN KEY (created_by)
 13    REFERENCES application_user (application_user_id)
 14  , CONSTRAINT employee_fk2 FOREIGN KEY (last_updated_by)
 15    REFERENCES application_user (application_user_id));

You create the employee_seq sequence with this statement:

SQL> CREATE SEQUENCE employee_seq;

Next, you create a trigger to generate sequence values like you did for the application_user table:

SQL> CREATE OR REPLACE TRIGGER employee_t1
  2    BEFORE INSERT ON employee
  3    FOR EACH ROW
  4  BEGIN
  5    /* Check for a empty image_id primary key column value,
  6	  and assign the next sequence value when it is missing. */
  7    IF :NEW.employee_id IS NULL THEN
  8	 SELECT employee_seq.NEXTVAL
  9	 INTO	:NEW.employee_id
 10	 FROM	dual;
 11    END IF;
 12  END;
 13  /

You have created the two tables for our non-critical trigger. The next section relies on the framework and integrates with it.

Non-critical Trigger

Before you create the logging trigger, you should test the concept of replacing a whitespace in a multipart last name with a hyphenated name. The following INSERT trigger fixes user input by replacing the whitespace with a hyphen. It doesn’t log the entry and some times you won’t log results for this type of trigger.

You create the employee_t2 trigger with the following:

SQL> CREATE OR REPLACE TRIGGER employee_t2
  2    BEFORE INSERT ON employee
  3    FOR EACH ROW
  4    FOLLOWS employee_t1
  5    WHEN (REGEXP_LIKE(NEW.last_name,' '))
  6  BEGIN
  7    /* Substitute a dash for the white space. */
  8    :NEW.last_name := REGEXP_REPLACE(:NEW.last_name,' ','-',1,1);
  9  END;
 10  /

Line 4 designates that employee_t2 executes after employee_t1, which is the purpose of the FOLLOWS command. Line 8 uses the REGEXP_REPLACE function to find and replace the first instance of a whitespace with a hyphen.

After creating the employee_t2 trigger, you can test it by using an INSERT statement like this:

SQL> INSERT INTO employee
  2  ( employee_number
  3  , first_name
  4  , last_name
  5  , created_by
  6  , creation_date
  7  , last_updated_by
  8  , last_update_date )
  9  VALUES
 10  ('B12345-678'
 11  ,'Sandy'
 12  ,'Johnston Smith'
 13  , 1
 14  , TRUNC(SYSDATE)
 15  , 1
 16  , TRUNC(SYSDATE));

You can verify that the employee_t1 trigger prevented the entry of a multipart last name with the following query:

SQL> COLUMN employee_id     FORMAT 9999 HEADING "Employee|ID #"
SQL> COLUMN employee_number FORMAT A10  HEADING "Employee|Number"
SQL> COLUMN first_name      FORMAT A20  HEADING "First Name"
SQL> COLUMN last_name       FORMAT A20  HEADING "Last Name"
SQL> SELECT   employee_id
  2  ,        employee_number
  3  ,        first_name
  4  ,        last_name
  5  FROM     employee;

It returns:

Employee Employee
    ID # Number     First Name		 Last Name
-------- ---------- -------------------- --------------------
       1 B12345-678 Sandy                Johnston-Smith

As you see from the results, the last name is hyphenated. If we accept another use case for the UPDATE statement, we may treat updates like you treat inserts.

An INSERT trigger doesn’t guarantee the user can’t change the hyphenated last name into a multipart last name. The application user can always change the value by using an UPDATE statement. That’s why there must be an UPDATE trigger.

The first element of a our

SQL> CREATE OR REPLACE TRIGGER employee_t3
  2    BEFORE UPDATE OF last_name ON employee
  3    FOR EACH ROW
  4    WHEN (REGEXP_LIKE(NEW.last_name,' '))
  5  BEGIN
  6    /* Substitute a dash for the white space. */
  7    :NEW.last_name := REGEXP_REPLACE(:NEW.last_name,' ','-',1,1);
  8  END;
  9  /

Line 2 guarantees that the UPDATE trigger only runs when an UPDATE statement changes the last_name column of the employee table. An UPDATE statement like the following causes the trigger to run (technically, the jargon is “fire”):

SQL> UPDATE   employee
  2  SET      last_name = 'Johnston Smith'
  3  WHERE    employee_number = 'B12345-678';

Having shown you how to create the non-critical INSERT and UPDATE triggers, I’ll now show you how to create the following employee_log table. This is where you can store the results from INSERT, UPDATE, and DELETE triggers. All columns are nullable (or optional) columns except the sequence generated employee_log_id column. The columns are optional because an INSERT statement never has an old set of values, and a DELETE statement never has a new set of values. Only the UPDATE statement provides old and new values inside a trigger.

The following creates the employee_log table:

SQL> CREATE TABLE employee_log
  2  ( employee_log_id         NUMBER
  3  , employee_event          VARCHAR2(6)
  4  , old_employee_id         NUMBER
  5  , old_employee_number     VARCHAR2(10)
  6  , old_first_name          VARCHAR2(20)
  7  , old_middle_name         VARCHAR2(20)
  8  , old_last_name           VARCHAR2(20)
  9  , old_created_by          NUMBER
 10  , old_creation_date       DATE
 11  , old_last_updated_by     NUMBER
 12  , old_last_update_date    DATE
 13  , new_employee_id         NUMBER
 14  , new_employee_number     VARCHAR2(10)
 15  , new_first_name          VARCHAR2(20)
 16  , new_middle_name         VARCHAR2(20)
 17  , new_last_name           VARCHAR2(20)
 18  , new_created_by          NUMBER
 19  , new_creation_date       DATE
 20  , new_last_updated_by     NUMBER
 21  , new_last_update_date    DATE
 22  , CONSTRAINT employee_log_pk  PRIMARY KEY (employee_log_id));

You should create the employee_log_seq sequence, like

SQL> CREATE SEQUENCE employee_log_seq;

Then, you should add an employee_log_t1 trigger to generate the sequence value automatically. The trigger follows the pattern of the prior two triggers for the application_user and employee tables.

You create the employee_log_seq trigger with the following syntax:

SQL> CREATE OR REPLACE TRIGGER employee_log_t1
  2    BEFORE INSERT ON employee_log
  3    FOR EACH ROW
  4  BEGIN
  5    /* Check for a empty image_id primary key column value,
  6	  and assign the next sequence value when it is missing. */
  7    IF :NEW.employee_log_id IS NULL THEN
  8	 SELECT employee_log_seq.NEXTVAL
  9	 INTO	:NEW.employee_log_id
 10	 FROM	dual;
 11    END IF;
 12  END;
 13  /

The logging table is the first step. After creating the logging table, you need to create a standalone log_invalid_employee procedure. The following code creates the procedure. This procedure only runs in the current transaction context, and later another version shows you how to implement it in an autonomous transaction context.

SQL> CREATE OR REPLACE
  2    PROCEDURE log_invalid_employee
  3    ( pv_employee_event	  VARCHAR2
  4    , pv_old_employee_id	  NUMBER
  5    , pv_old_employee_number   VARCHAR2
  6    , pv_old_first_name	  VARCHAR2
  7    , pv_old_last_name	  VARCHAR2
  8    , pv_old_created_by	  NUMBER
  9    , pv_old_creation_date	  DATE
 10    , pv_old_last_updated_by   NUMBER
 11    , pv_old_last_update_date  DATE
 12    , pv_new_employee_id	  NUMBER
 13    , pv_new_employee_number   VARCHAR2
 14    , pv_new_first_name	  VARCHAR2
 15    , pv_new_last_name	  VARCHAR2
 16    , pv_new_created_by	  NUMBER
 17    , pv_new_creation_date	  DATE
 18    , pv_new_last_updated_by   NUMBER
 19    , pv_new_last_update_date  DATE) IS
 20  BEGIN
 21    /* Write to the log table. */
 22    INSERT INTO employee_log
 23    ( employee_event
 24    , old_employee_id
 25    , old_employee_number
 26    , old_first_name
 27    , old_last_name
 28    , old_created_by
 29    , old_creation_date
 30    , old_last_updated_by
 31    , old_last_update_date
 32    , new_employee_id
 33    , new_employee_number
 34    , new_first_name
 35    , new_last_name
 36    , new_created_by
 37    , new_creation_date
 38    , new_last_updated_by
 39    , new_last_update_date )
 40    VALUES
 41    ( pv_employee_event
 42    , pv_old_employee_id
 43    , pv_old_employee_number
 44    , pv_old_first_name
 45    , pv_old_last_name
 46    , pv_old_created_by
 47    , pv_old_creation_date
 48    , pv_old_last_updated_by
 49    , pv_old_last_update_date
 50    , pv_new_employee_id
 51    , pv_new_employee_number
 52    , pv_new_first_name
 53    , pv_new_last_name
 54    , pv_new_created_by
 55    , pv_new_creation_date
 56    , pv_new_last_updated_by
 57    , pv_new_last_update_date );
 58  END log_invalid_employee;
 59  /

With the logging table and procedure, you can now rework the INSERT and UPDATE triggers into a single trigger. The new trigger fires when an INSERT or an UPDATE statement affects the employee table. That means you can log the data from both events.

If you created employee_t1, employee_t2 and employee_t3 triggers, you need to drop employee_t2 and employee_t3 triggers before creating the new trigger. The previous employee_t3 trigger will cause incorrect behaviors because it is incompatible with the new employee_t1 trigger.

The new employee_t1 trigger is:

SQL> CREATE OR REPLACE TRIGGER employee_t1
  2    BEFORE INSERT OR UPDATE OF last_name ON employee
  3    FOR EACH ROW
  4    WHEN (REGEXP_LIKE(NEW.last_name,' '))
  5  DECLARE
  6    /* DML event label. */
  7    lv_employee_event  VARCHAR2(6);
  8  BEGIN
  9    /* Check for an event and assign event value. */
 10    IF INSERTING THEN
 11	 /* Check for a empty image_id primary key column value,
 12	    and assign the next sequence value when it is missing. */
 13	 IF :NEW.employee_id IS NULL THEN
 14	   SELECT employee_seq.NEXTVAL
 15	   INTO   :NEW.employee_id
 16	   FROM   dual;
 17	 END IF;
 18	 lv_employee_event := 'INSERT';
 19    ELSE
 20	 lv_employee_event := 'UPDATE';
 21    END IF;
 22
 23    /* Log the details captured by an insert or update. */
 24    log_invalid_employee
 25    ( pv_employee_event => lv_employee_event
 26    , pv_old_employee_id => :old.employee_id
 27    , pv_old_employee_number => :old.employee_number
 28    , pv_old_first_name => :old.first_name
 29    , pv_old_last_name => :old.last_name
 30    , pv_old_created_by => :old.created_by
 31    , pv_old_creation_date => :old.creation_date
 32    , pv_old_last_updated_by => :old.last_updated_by
 33    , pv_old_last_update_date => :old.last_update_date
 34    , pv_new_employee_id => :NEW.employee_id
 35    , pv_new_employee_number => :NEW.employee_number
 36    , pv_new_first_name => :NEW.first_name
 37    , pv_new_last_name => :NEW.last_name
 38    , pv_new_created_by => :NEW.created_by
 39    , pv_new_creation_date => :NEW.creation_date
 40    , pv_new_last_updated_by => :NEW.last_updated_by
 41    , pv_new_last_update_date => :NEW.last_update_date );
 42
 43    /* Substitute a dash for the white space. */
 44    :NEW.last_name := REGEXP_REPLACE(:NEW.last_name,' ','-',1,1);
 45  END;
 46  /

This non-critical trigger checks whether the event is an INSERT statement on line 10. The trigger generates a sequence value when an INSERT statement fires the trigger. Then, the trigger sets a local variable with the INSERT string. It assigns an UPDATE string when an UPDATE statement fires the trigger.

After the event detection logic, the trigger calls the log_invalid_employee procedure on line 24. Line 44 changes the multipart last name into a hyphenated last name.

This part of the article has shown you how to create and manage non-critical triggers.

Critical Trigger

This part of the article shows you how to create and manage critical triggers. The key difference is that critical triggers stop the transaction that fires the trigger. This has significant impact on how you design and implement the log_invalid_employee procedure.

You need to modify the log_invalid_employee procedure so that it supports autonomous transactions. That requires adding a PRAGMA precompiler directive in the declaration block and a COMMIT statement after the INSERT statement.

The following shows you the changes required in the log_invalid_employee procedure:

SQL> CREATE OR REPLACE
  2    PROCEDURE log_invalid_employee
  3    ( pv_employee_event         VARCHAR2
  4    , pv_old_employee_id        NUMBER
 ...
 21    /* Set precompiler directive to run in a separate context. */
 22    PRAGMA AUTONOMOUS_TRANSACTION;
 23  BEGIN
 ...
 62    /* Commit the autonmous transaction. */
 63    COMMIT;
 64  END log_invalid_employee;
 65  /

Line 22 holds the autonomous transaction PRAGMA, and line 63 holds the COMMIT statement. Both of these are required when you want to enable a trigger to both log data and raise an exception that terminates the transaction.

Next, you need to rework the employee_t1 trigger by adding content to the declaration and execution blocks, and by adding an exception block. The declaration block requires you to declare an exception variable and create a PRAGMA precompiler directive. The exception block requires you to add a conditional block at the end of the execution block. You also need to add an exception block to manage a raised exception.

The following shows you the changes required for the employee_t1 trigger:

SQL> CREATE OR REPLACE TRIGGER employee_t1
  2    BEFORE INSERT OR UPDATE OF last_name ON employee
  3    FOR EACH ROW
  4    WHEN (REGEXP_LIKE(NEW.last_name,' '))
  5  DECLARE
 ...
  9    /* Declare exception. */
 10    e EXCEPTION;
 11    PRAGMA EXCEPTION_INIT(e,-20001);
 12  BEGIN
 ...
 47    /* Re-check for an event and assign event value. */
 48    IF INSERTING THEN
 49      /* Substitute a dash for the white space. */
 50      :NEW.last_name := REGEXP_REPLACE(:NEW.last_name,' ','-',1,1);
 51    ELSE
 52      /* Throw exception. */
 53      RAISE_APPLICATION_ERROR(-20001,'No two-part last names without a hyphen.');
 54    END IF;
 55  EXCEPTION
 56    /* Capture an exception. */
 57    WHEN e THEN
 58      ROLLBACK;
 59	 dbms_output.put_line('[Trigger Event: '||lv_employee_event||']');
 60	 dbms_output.put_line(SQLERRM);
 61    WHEN OTHERS THEN
 62	 dbms_output.put_line(SQLERRM);
 63  END;
 64  /

Line 10 declares a local exception variable. Line 11 declares the PRAGMA precompiler directive. Lines 47 through 54 implements the conditional logic for writing a hyphenated last name for an INSERT statement, and the logic for raising an exception for an UPDATE statement.

An INSERT statement causes the database trigger to write to the employee_log logging table. An UPDATE statement causes the database trigger to write to the logging table and throw an exception.

The test case for a critical event trigger uses an UPDATE statement, as shown:

SQL> UPDATE   employee
  2  SET      last_name = 'Zeta Jones'
  3  WHERE    employee_number = 'B98765-678';

The first thing you see is a thrown exception, like

[Trigger Event: UPDATE]
ORA-20001: No two-part last names without a hyphen.

After you see the thrown exception, you can run the following query to see what has been written to the exception_log table:

SQL> COLUMN employee_log_id  FORMAT 9999 HEADING "Empl|Log|ID #"
SQL> COLUMN old_employee_id  FORMAT 9999 HEADING "Empl|ID #"
SQL> COLUMN old_name         FORMAT A25  HEADING "Old Name"
SQL> COLUMN new_employee_id  FORMAT 9999 HEADING "Empl|ID #"
SQL> COLUMN new_name         FORMAT A25  HEADING "New Name"
SQL> SELECT   employee_log_id
  2  ,        old_employee_id
  3  ,        DECODE( old_last_name || ', '|| old_first_name,', ',NULL
  4                 , old_last_name || ', '|| old_first_name) AS "old_name"
  5  ,        new_employee_id
  6  ,        DECODE( new_last_name || ', '|| new_first_name,', ',NULL
  7                 , new_last_name || ', '|| new_first_name) AS "new_name"
  8  FROM     employee_log;

It displays:

 Empl
  Log  Empl                            Empl
 ID #  ID # Old Name                   ID # New Name
----- ----- ------------------------- ----- -------------------------
    1                                     2 Evert Lloyd, Chris
    2     2 Evert-Lloyd, Chris            2 Evert Lloyd, Chris
    3                                     3 Zeta Jones, Catherine
    4     3 Zeta-Jones, Catherine         3 Zeta Jones, Catherine

The ultimate test of these is that while there were many attempts at entering a multipart last name, none of them succeeds. You can query the last_name column from the employee table to verify that, like

SQL> SELECT   last_name
  2  FROM     employee;

It should show you the three rows that you’ve inserted and updated through this article. You should see:

Last Name
-------------------------
Johnston-Smith
Evert-Lloyd
Zeta-Jones

Through this article you should have learned how to create non-critical and critical triggers. These techniques are important when you manage transactions against business rules that can’t be supported by ordinary database constraints.

Written by maclochlainn

November 25th, 2018 at 5:26 pm