(ref) INSTEAD OF triggers

NON-UPDATABLE Views contain any:
  • One or more Table Joins.
  • A GROUP BY clause.
  • START WITH ...CONNECT BY.
  • DISTINCT.
  • Aggregate function (i.e. SUM, AVG, MIN, MAX, COUNT).
  • Set operators.
  • Derived columns (i.e. concatenated, pseudocolumns, etc).

INSTEAD OF triggers

INSTEAD OF triggers are created on
  • (usually) a non-updatable view
  • a nested table column of a non-updatable view

What?
  • INSTEAD OF trigger cannot be conditional.
  • Is the only way to update a view that is not inherently updatable
  • Is always a row-level trigger
  • Can read :OLD and :NEW, but cannot change them.
  • The timing events (BEFORE and AFTER) is not applicable for INSTEAD OF triggers.
  • INSTEAD OF trigger can fire for all three DML statements (I/U/D).
  • Prior to Oracle8.1.6 INSTEAD OF triggers functionality was included in the Enterprise Edition only.

Why use?
  • Used to intercept DML statements and replace them with alternative code.
  • Useful when application does not need (or should not) see the underlying tables (i.e. for security reasons). In this case:
    1. The application issues DMLs against the view
    2. The INSTEAD OF trigger intercepts and rewrite the DMLs to the underlying tables.
  • Useful also when the system uses object-relational functionality.
  • Updates against complex views may result in "ORA-01776-cannot modify more than one base table through a join view" or "ORA-01779-cannot modify a column which maps to a non key-preserved table"
How?
  • The database fires the INSTEAD OF trigger instead of running the triggering DML statement.
  • The trigger should: (a) determine what operation was intended and (b) perform the appropriate DML operations on the underlying tables.
Creation Syntax:
CREATE [OR REPLACE] TRIGGER trigger_name
 INSTEAD OF {dml_statement }
 ON {object_name | database | schema}
 FOR EACH ROW
 [DISABLE]
 [WHEN (logical_expression)]
 [DECLARE]
  declaration_statements;
BEGIN
  execution_statements;
END [trigger_name];
/
In the example below (taken from here) you:

(1) Create the base tables Custormer and Orders.
(2) Create a view order_info.
(3) Create a INSTEAD OF trigger order_info_insert that translates the DML statement into statements acting on the base tables (Orders and Customers).
(4) Insert data on the order_info view. Check the insertion on the base tables.

(1-2) Create base tables Customer, Orders and view.
SQL> CREATE TABLE customers ( 
     customer_id NUMBER (6)  NOT NULL  PRIMARY KEY, 
     cust_first_name VARCHAR2 (20)  NOT NULL , 
     cust_last_name VARCHAR2 (20)  NOT NULL , 
     cust_address VARCHAR2 (200) , 
     phone_numbers VARCHAR2 (12) , 
     nls_language VARCHAR2 (3) , 
     nls_territory VARCHAR2 (30) , 
     credit_limit NUMBER (9,2) , 
     cust_email VARCHAR2 (30) , 
     account_mgr_id NUMBER (6));

Table created.
SQL> CREATE TABLE orders ( 
     order_id NUMBER (12)  NOT NULL Primary key, 
     order_date TIMESTAMP  NOT NULL , 
     order_mode VARCHAR2 (8) , 
     customer_id NUMBER (6)  NOT NULL , 
     order_status NUMBER (2) , 
     order_total NUMBER (8,2) , 
     sales_rep_id NUMBER (6) , 
     promotion_id NUMBER (6));

Table created.
SQL> CREATE OR REPLACE VIEW order_info AS
   SELECT c.customer_id, c.cust_last_name, c.cust_first_name,
          o.order_id, o.order_date, o.order_status
   FROM customers c, orders o
   WHERE c.customer_id = o.customer_id;

View created.
SQL>

Now, if you try to perform an INSERT in the view order_info, it will fail with an ORA-01779 error:

SQL>INSERT INTO order_info 
    values (345, 'Rogers', 'John', 1250, sysdate, 1);

Error: ORA-01779: "cannot modify a column which maps to a non key-preserved table"
*Cause:    An attempt was made to insert or update columns of a join view which
           map to a non-key-preserved table.
*Action:   Modify the underlying base tables directly.


A Key preserved table means that the row from the base table will appear AT MOST ONCE in the output view on that table. Here one customer clearly may have multiple orders, so customer_id will most probably appear multiple times on order_info.

So, if you issue an INSERT against order_info, how can the database decide what to do with it?
Should it insert into orders AND customers? Only into orders? Or only into customers?

Well, you need to write an instead of trigger to intercept the DML against the trigger and issue DMLs against the base table(s) according to your business rules..

In this example, the trigger will split the INSERT coming from the application and insert on each table accordingly:


(3) Create a INSTEAD OF trigger order_info_insert that translates the DML statement into statements acting on the base tables (Orders and Customers).
SQL> CREATE OR REPLACE TRIGGER order_info_insert
  INSTEAD OF INSERT ON order_info
DECLARE
 duplicate_info EXCEPTION;
 PRAGMA EXCEPTION_INIT (duplicate_info, -00001);
BEGIN
 INSERT INTO Customers
   (customer_id, cust_last_name, cust_first_name)
 VALUES (:new.customer_id, :new.cust_last_name, :new.cust_first_name);
 INSERT INTO Orders (order_id, order_date, customer_id, order_status)
 VALUES (:new.order_id, :new.order_date, :new.customer_id, :new.order_status);
EXCEPTION
 WHEN duplicate_info THEN
   raise_application_error (
     num=> -20107,
     msg=> 'Duplicate customer or order ID');
END order_info_insert;
/
Trigger created.
SQL>

(4) Insert data on the order_info view. Check the insertion on the base tables.
SQL> INSERT into order_info 
 (Customer_id, cust_last_name, cust_first_name, order_id, order_date, order_status)
VALUES
 (1, 'Smith', 'John', 2500, '13-AUG-09', 0);
1 rows inserted.


SQL> Select Customer_id, cust_last_name, cust_first_name from customers;

CUSTOMER_ID  CUST_FIRST_NAME  CUST_LAST_NAME       
------------ ---------------- ----------------- 
           1 John             Smith 

SQL> Select order_id, order_date, order_status from orders;

ORDER_ID    ORDER_DATE                              ORDER_STATUS
---------- ---------------------------------------- ------------------------
     2500  13-AUG-09 12.00.00.000000 AM             0

SQL>

INSTEAD OF triggers on Nested Table Column of View


Nested Table Column of View (?)

You can create a view that has a nested table as one of its columns.
Now if you want to be able to update such a view, you need a instead of trigger.
It will intercept the object-relational INSERT and translate it to one that inserts into the base table.



(1) To create a view with a nested table as one of its columns you can:

(a) create the object type for the elements of the nested table. In this case, the type nte captures attributes of employees.

CREATE OR REPLACE TYPE nte AUTHID DEFINER IS
 OBJECT (
   emp_id   number(6),
   lastname varchar2(25),
   job      varchar2(10),
   sal      number(8,2));


(b) create the object table type. Here it will define a table of objects of the type nte.

CREATE OR REPLACE TYPE emp_list_ IS TABLE OF nte;

(c) Now you define the view containing the nested table in one of its columns (emplist).

CREATE OR REPLACE VIEW dept_view AS
  SELECT d.department_id,
  d.department_name,
  CAST (MULTISET (SELECT e.employee_id, e.last_name, e.job_id, i.salary
                  FROM employees e
                  WHERE e.department_id = d.department_id) 
                  AS emp_list_
                 ) emplist
  FROM departments d; 

(d) You can select the contents of emplist, and compare with a direct select of the base table:

SQL> select emplist from dept_view where department_id=10;

EMPLIST(EMP_ID, LASTNAME, JOB, SAL)
-----------------------------------------------
EMP_LIST_(NTE(200, 'Whalen', 'AD_ASST', 4400)


SQL> SELECT employee_id, last_name, job_id, salary
     FROM employees
     WHERE department_id = 10;

EMPLOYEE_ID LAST_NAME		      JOB_ID	     SALARY
----------- ------------------------- ---------- ----------
200 Whalen		      AD_ASST	       4400

(e) Now, if you try to insert directly into the dept_view, you'll receive an ORA-25015.

SQL> INSERT into table (
                        select d.emplist
                        from dept_view d
                        where department_id = 10
     ) 
     VALUES (1001, 'Glenn', 'AC_MGR', 10000);

SQL Error: ORA-25015: cannot perform DML on this nested table view column
*Cause:    DML cannot be performed on a nested table view column except through
an INSTEAD OF trigger
*Action:   Create an INSTEAD OF trigger over the nested table view column
and then perform the DML.

(f) So you need an INSTEAD OF trigger:

CREATE OR REPLACE TRIGGER dept_emplist_tr
 INSTEAD OF INSERT ON NESTED TABLE emplist OF dept_view
 REFERENCING NEW as Employee
             PARENT as Department
 FOR EACH ROW
BEGIN
  -- Intercept the insert into the nested table.
  -- Translate it to an insert into the base table.
  INSERT INTO employees (
              employee_id, last_name, email, hire_date, job_id, salary, department_id)
  VALUES (
     :Employee.emp_id,
     :Employee.lastname,
     :Employee..lastname || '@company.com', 
     SYSDATE,
     :Employee.job,
     :Employee.sal,
     :Department.department_id);
END;


(g) Now you if repeat the insert statement, the trigger will fire and perform the insert in the base table employees:

SQL> INSERT into table (
                        select d.emplist
                        from dept_view d
                        where department_id = 10
     ) 
     VALUES (1001, 'Glenn', 'AC_MGR', 10000);

1 rows inserted.

(h) You can check the results by querying the dept_view view and employees table:

SQL> select emplist from dept_view where department_id = 10;

EMPLIST(EMP_ID, LASTNAME, JOB, SAL)
--------------------------------------------------------------------------------
EMP_LIST_(NTE(200, 'Whalen', 'AD_ASST', 4400), NTE(1001, 'Glenn', 'AC_MGR', 10000))


SQL> SELECT employee_id, last_name, job_id, salary
     FROM employees
     WHERE department_id = 10;

EMPLOYEE_ID LAST_NAME		      JOB_ID	     SALARY
----------- ------------------------- ---------- ----------
	200 Whalen		      AD_ASST	       4400
       1001 Glenn		      AC_MGR	      10000

2 rows selected.



No comments:

Post a Comment