Oracle Trigger Basics
Oracle Trigger Basics
Once you master the basics of inserting, updating, and deleting data from the database, you typically learn about database triggers. Database triggers are coding elements that let you manage events beyond the limit of database constraints.
Before you can appreciate the power of database triggers, you need to understand what database constraints can and can’t do. Then, together we’ll explore how you can implement database triggers.
Database constraints let you manage events. A primary key constraint guarantees a column or a set of columns are unique and not null. A foreign key constraint guarantees a column only contains a value or set of values found in the primary key. A not null constraint makes a column mandatory when you insert or update a row in a table. A unique constraint guarantees a column or set of columns only exist in one row of a table. A check constraint guarantees a column’s value must comply with a set of rules defined with a row of data.
Database constraints do have limits. For example, a foreign key constraint doesn’t guarantee the right foreign key value because it only guarantees a foreign key value is a valid value in a list of possible values. That means it’s possible to insert or update a foreign key column or set of columns with an incorrect foreign key value. Only a database trigger can guarantee the insert or update of a correct foreign key value. The database trigger verifies the correct behavior by validating conditions before an insert or update.
While a unique constraint guarantees uniqueness and a check constraint guarantees compliance against a set of rules in a row, only a database trigger can guarantee the maximum number of like rows in a table that comply with a rule. Also, there is no constraint that manages inserts, updates, and deletes with dependencies on data in other tables.
A Data Manipulation Language (DML) trigger lets you manage these shortfalls and more. You have two options when implementing DML triggers. One implements a statement trigger and the other implements a row-level trigger. A statement-level trigger runs once for any and all rows affected by an INSERT
, UPDATE
, or DELETE
statement. A row-level trigger runs once for each row affected by an INSERT
, UPDATE
, or DELETE
statement.
Both of these triggers have two components – a trigger and a trigger body. The trigger defines what event to manage and the trigger body implements the logic that manages the event.
Statement-Level Triggers
You should create two tables to work with statement-level triggers. The first is the avenger table and the second is the avenger_log table. Your inserts, updates, and deletes to the avenger table act as events that fire triggers. Statement-level triggers can be defined to run before or after INSERT
, UPDATE
, and DELETE
statements. Statement-level triggers are narrowly scoped events and they log message data to the avenger_log
table.
This is the definition of the avenger table:
SQL> CREATE TABLE avenger 2 ( avenger_id NUMBER 3 , avenger_name VARCHAR2(30) 4 , first_name VARCHAR2(20) 5 , last_name VARCHAR2(20)); |
This is the definition of the avenger_log
table:
SQL> CREATE TABLE avenger_log 2 ( avenger_log_id NUMBER 3 , trigger_name VARCHAR2(30) 4 , trigger_timing VARCHAR2(6) 5 , trigger_event VARCHAR2(6) 6 , trigger_type VARCHAR2(12)); |
The following avenger_t1
creates a BEFORE INSERT
statement trigger:
SQL> CREATE OR REPLACE TRIGGER avenger_t1 2 BEFORE INSERT ON avenger 3 DECLARE 4 /* Declare local trigger-scope variables. */ 5 lv_sequence_id NUMBER := avenger_log_s.NEXTVAL; 6 lv_trigger_name VARCHAR2(30) := 'AVENGER_T1'; 7 lv_trigger_event VARCHAR2(6) := 'INSERT'; 8 lv_trigger_type VARCHAR2(12) := 'STATEMENT'; 9 lv_trigger_timing VARCHAR2(6) := 'BEFORE'; 10 BEGIN 11 /* Log event into the avenger_log table. */ 12 INSERT INTO avenger_log 13 ( avenger_log_id 14 , trigger_name 15 , trigger_event 16 , trigger_type 17 , trigger_timing ) 18 VALUES 19 ( lv_sequence_id 20 , lv_trigger_name 21 , lv_trigger_event 22 , lv_trigger_type 23 , lv_trigger_timing ); 24 END avenger_t1; 25 / |
Lines 1 and 2 declare the trigger. Lines 3 through 24 implements an anonymous PL/SQL block as the trigger’s body, and line 25 executes the trigger. Lines 6 through 9 store literal values for the trigger’s name, event, type, and timing. The trigger uses these literal values when logging events to the avenger_log
table.
You access the data catalog information about triggers in the DBA_
, ALL_
, and USER_TRIGGERS
views in a non-containerized database. Triggers also exist in those views for containerized databases (CDB). CDBs have an additional CDB_TRIGGERS
view that stores triggers. The trigger body is stored in the TRIGGER_BODY
column of those views in a LONG
column.
You can create an AFTER STATEMENT
trigger by simply changing the first two lines or the trigger declaration, as follows:
SQL> CREATE OR REPLACE TRIGGER avenger_t1 2 AFTER INSERT ON avenger |
Then, you need to change values of the string literals on lines 6, 7, and 9 as follows:
6 lv_trigger_name VARCHAR2(30) := 'AVENGER_T2'; 7 lv_trigger_event VARCHAR2(6) := 'INSERT'; 8 lv_trigger_type VARCHAR2(12) := 'STATEMENT'; 9 lv_trigger_timing VARCHAR2(6) := 'AFTER'; |
Compiling the database triggers, let’s insert a row into the avenger
table, like this:
SQL> INSERT INTO avenger 2 VALUES 3 ( avenger_s.NEXTVAL 4 ,'Captain America' 5 ,'Steven' 6 ,'Rogers'); |
Then, you can query the avenger_log
table, like this:
SQL> COLUMN avenger_log_id FORMAT 999 HEADING "Avenger|Log ID #" SQL> COLUMN trigger_name FORMAT A12 HEADING "Trigger|Name" SQL> COLUMN trigger_timing FORMAT A7 HEADING "Trigger|Timing" SQL> COLUMN trigger_event FORMAT A7 HEADING "Trigger|Event" SQL> COLUMN trigger_type FORMAT A12 HEADING "Trigger|Type" SQL> SELECT * FROM avenger_log; |
It returns two rows – one row from the avenger_t1
trigger and the other from the avenger_t2
trigger:
Avenger TRIGGER TRIGGER TRIGGER TRIGGER LOG ID # Name TIMING Event TYPE -------- ------------ ------- ------- ---------- 1 AVENGER_T2 AFTER INSERT STATEMENT 2 AVENGER_T1 BEFORE INSERT STATEMENT |
Both of the triggers use the avenger_log_s1
sequence. You may notice that the AFTER STATEMENT
trigger ran before the BEFORE STATEMENT
trigger. That shows you that triggers aren’t sequenced by default, even when you think that the timing event should sequence them.
Oracle lets you sequence triggers by using the FOLLOWS
clause when you define database triggers. The following modifies the avenger_t2
by adding a FOLLOWS
clause on line 3, and it uses ellipses to shorten the example:
SQL> CREATE OR REPLACE TRIGGER avenger_t2 2 BEFORE INSERT ON avenger 3 FOLLOWS avenger_t1 4 DECLARE … 11 BEGIN … 25 END avenger_t2; 26 / |
The testing script drops and creates the avenger_log
table before creating fresh copies of the avenger_t1
and avenger_t2
triggers. The script lets you re-query the avenger_log
table without the baggage of the previous two rows.
Like before, it returns two rows – one row from the avenger_t1
trigger and the other from the avenger_t2
trigger:
Avenger TRIGGER TRIGGER TRIGGER TRIGGER LOG ID # Name TIMING Event TYPE -------- ------------ ------- ------- ----------- 1 AVENGER_T1 BEFORE INSERT STATEMENT 2 AVENGER_T2 AFTER INSERT STATEMENT |
You should note that the BEFORE STATEMENT
trigger now runs before the AFTER STATEMENT
trigger. The FOLLOWS
clause lets you guarantee the order of trigger execution.
As you can see, statement-level triggers don’t give us the ability to see, change, or log the before and after values of data. You can do that with row-level triggers.
Row-Level Triggers
Row-level triggers let you see the initial column values you add into a table with an INSERT
statement. Row-level triggers let you see existing column values and the column values provided by an UPDATE
statement. The DELETE
statement only provides the existing column values to a trigger because it removes the row from the database. Inside the row-level trigger you can change new values based on rules that you put in code inside the database trigger.
The avenger_log
table requires major changes to support a row-level database trigger because it needs to store the old and new values of a table’s data column. Data columns hold values that describe an instance of the table. A data column or set of data columns should also define a unique key that makes each row unique in a table.
After engineering a table, you should also add a surrogate key column. A surrogate (stand-in) key column contains a value generated from a sequence, and a surrogate key is generally unrelated to the subject of a table. You use the natural key to find a unique row in a table, and you copy the surrogate key value when you want a foreign key to link another row with the row identified by the surrogate key.
Both the surrogate key column and natural key (one or more columns) should both identify unique rows. That means for every surrogate key there should be a natural key.
The data columns in the avenger table are the avenger_name
, first_name
, and last_name
columns. You should define an old
and new
column for each of the data columns when you create a logging table.
This defines the new avenger_log
table:
SQL> CREATE TABLE avenger_log 2 ( avenger_log_id NUMBER 3 , trigger_name VARCHAR2(30) 4 , trigger_timing VARCHAR2(6) 5 , trigger_event VARCHAR2(6) 6 , trigger_type VARCHAR2(12) 7 , old_avenger_name VARCHAR2(20) 8 , old_first_name VARCHAR2(20) 9 , old_last_name VARCHAR2(20) 10 , new_avenger_name VARCHAR2(20) 11 , new_first_name VARCHAR2(20) 12 , new_last_name VARCHAR2(20)); |
The first row-level database trigger you create runs when an INSERT
statement adds a new row to the avenger
table. The code exists below:
SQL> CREATE OR REPLACE TRIGGER avenger_t3 2 BEFORE INSERT ON avenger 3 FOR EACH ROW 4 DECLARE 5 /* Declare local trigger-scope variables. */ 6 lv_sequence_id NUMBER := avenger_log_s.NEXTVAL; 7 lv_trigger_name VARCHAR2(30) := 'AVENGER_T3'; 8 lv_trigger_event VARCHAR2(6) := 'INSERT'; 9 lv_trigger_type VARCHAR2(12) := 'FOR EACH ROW'; 10 lv_trigger_timing VARCHAR2(6) := 'BEFORE'; 11 BEGIN 12 /* Log event into the avenger_log table. */ 13 INSERT INTO avenger_log 14 ( avenger_log_id 15 , trigger_name 16 , trigger_event 17 , trigger_type 18 , trigger_timing 19 , old_avenger_name 20 , old_first_name 21 , old_last_name 22 , new_avenger_name 23 , new_first_name 24 , new_last_name ) 25 VALUES 26 ( lv_sequence_id 27 , lv_trigger_name 28 , lv_trigger_event 29 , lv_trigger_type 30 , lv_trigger_timing 31 , :old.avenger_name 32 , :old.first_name 33 , :old.last_name 34 , :NEW.avenger_name 35 , :NEW.first_name 36 , :NEW.last_name ); 37 END avenger_t3; 38 / |
Line 3 declares the avenger_t3
trigger as a row-level trigger. Lines 31 through 36 inserts the old and new values from the row of the avenger
table when the INSERT
statement runs with the following three values:
SQL> INSERT INTO avenger 2 VALUES 3 ( avenger_s.NEXTVAL 4 ,'Capt. America' 5 ,'Steven' 6 ,'Rogers'); |
Since the script drops and recreates the avenger
and avenger_log
tables and drops the avenger_t1
and avenger_t2
statement-level triggers, you can write a query to return only the test row. The following anonymous PL/SQL block let’s you print the old
and new
column values next to one another. The program helps make the row-level trigger’s ability to see before and after values clear.
SQL> SET SERVEROUTPUT ON SIZE UNLIMITED SQL> BEGIN 2 FOR i IN (SELECT * FROM avenger_log) LOOP 3 dbms_output.put_line( 4 'Trigger Name [' 5 || i.trigger_name||']'); 6 dbms_output.put_line( 7 'Trigger Event [' 8 || i.trigger_event||']'); 9 dbms_output.put_line( 10 'Trigger Type [' 11 || i.trigger_type||']'); 12 dbms_output.put_line( 13 'Trigger Timing [' 14 || i.trigger_timing||']'); 15 dbms_output.put_line( 16 'Avenger Name [' 17 || i.old_avenger_name||'][' 18 || i.new_avenger_name||']'); 19 dbms_output.put_line( 20 'First Name [' 21 || i.old_first_name||'][' 22 || i.new_first_name||']'); 23 dbms_output.put_line( 24 'Last Name [' 25 || i.old_last_name||'][' 26 || i.new_last_name||']'); 27 END LOOP; 28 END; 29 / |
This anonymous block prints the following from the avenger_log
table:
TRIGGER Name [AVENGER_T3] TRIGGER Event [INSERT] TRIGGER TYPE [FOR EACH ROW] TRIGGER TIMING [BEFORE] Avenger Name [][Capt. America] FIRST Name [][Steven] LAST Name [][Rogers] |
This has demonstrated how you write a row-level trigger against an INSERT
event. You should note that the old
values for the avenger_name
, first_name
, and last_name
are null values between the square brackets. Next, you should examine how to write a row-level trigger against more than one type of event.
The Oracle Database lets you write individual triggers for INSERT
, UPDATE
, or DELETE
statement, or a single trigger to manage INSERT
, UPDATE
, and DELETE
events. The following modifies the avenger_t3
trigger so that it works for an INSERT
, UPDATE
, and DELETE
events:
SQL> CREATE OR REPLACE TRIGGER avenger_t3 2 BEFORE INSERT OR UPDATE OR DELETE ON avenger 3 FOR EACH ROW |
Line 2 of the previous trigger is where we change the avenger_t3
trigger to also work with UPDATE
and DELETE
events. Then, we need to change one other line and then add a small IF
-block to the trigger.
Line 8 of the original trigger assigns a default value to the lv_trigger_event
variable, but you need to remove the value assignment. The modified line looks like this:
8 lv_trigger_event VARCHAR2(6); |
You also need to add an IF
-block that manages Data Manipulation Language (DML) event functions. The IF
-block should be the first thing in the execution block of the trigger body, and it should implement this logic:
11 BEGIN 12 /* Evaluate and assign event for logging. */ 13 IF INSERTING THEN lv_trigger_event := 'INSERT'; 14 ELSIF UPDATING THEN lv_trigger_event := 'UPDATE'; 15 ELSIF DELETING THEN lv_trigger_event := 'DELETE'; 16 END IF; ... |
The INSERTING
event function on line 13 occurs when an INSERT
statement activates the trigger. The UPDATING
and DELETING
event functions on lines 14 and 15 occur when a respective UPDATE
or DELETE
statement activity fires a trigger.
The following UPDATE
statement now creates an event that the avenger_t3
trigger is monitoring:
SQL> UPDATE avenger 2 SET avenger_name = 'Captain America' 3 WHERE avenger_name = 'Capt. America'; |
Next, let’s test a DELETE
statement with the following:
SQL> DELETE 2 FROM avenger 3 WHERE avenger_name = 'Captain America'; |
The following anonymous block program lets you see the log values inserted into the avenger_log
table from the INSERT
, UPDATE
, and DELETE
statement triggers:
SQL> SET SERVEROUTPUT ON SIZE UNLIMITED SQL> BEGIN 2 FOR i IN (SELECT * FROM avenger_log) LOOP 3 dbms_output.put_line( 4 'Row Number [' 5 || i.avenger_log_id ||'][' 6 || i.trigger_event ||']'); 7 dbms_output.put_line( 8 'Avenger Name [' 9 || i.old_avenger_name ||'][' 10 || i.new_avenger_name ||']'); 11 dbms_output.put_line( 12 'First Name [' 13 || i.old_first_name ||'][' 14 || i.new_first_name ||']'); 15 dbms_output.put_line( 16 'Last Name [' 17 || i.old_last_name ||'][' 18 || i.new_last_name ||']'); 19 END LOOP; 20 END; 21 / |
The anonymous block returns the following:
Row Number [1][INSERT] Avenger Name [][Capt. America] First Name [][Steven] Last Name [][Rogers] Row Number [2][UPDATE] Avenger Name [Capt. America][Captain America] First Name [Steven][Steven] Last Name [Rogers][Rogers] Row Number [3][DELETE] Avenger Name [Captain America][] First Name [Steven][] Last Name [Rogers][] |
You should notice the old
values for the INSERT
event are missing because there isn’t a row before running the INSERT
statement. Likewise, you should notice the new values for the DELETE
event are missing because there isn’t a row after running a DELETE
statement. Only the UPDATE
event has both an old
and new
value because the row exists before and after any change. The old
values hold the row’s values before the UPDATE
statement and the new
values hold the row’s values after the UPDATE
statement.