Update Trigger PL/SQL Oracle

2020-05-03 09:53发布

问题:

I have a table with the following structure:

create table treballa (
code varchar2(4), 
 name varchar2(20), 
 director varchar2(4), 
department number, 
salary int,
 primary key (code), 
 foreign key (director) references treballa(code) 
) 

And I need to create 1 trigger that checks if the department number of the updated worker, the total salary between all the workers of that department doesn't exceeds 10000, but I don't know how to do it really... Could you please help me? Thank you very much.

EDIT:

CREATE OR REPLACE TRIGGER controlsalaridept BEFORE INSERT OR UPDATE ON TREBALLA
FOR EACH ROW
  DECLARE
    salaritotal INT := 0;
  BEGIN
    IF INSERTING THEN
      SELECT sum(salary) INTO salaritotal FROM TREBALLA WHERE DEPARTMENT LIKE :new.DEPARTAMENT;
      DBMS_OUTPUT.PUT_LINE('Salari Total abans suma:'||salaritotal);
      salaritotal := salaritotal + :new.SALARY;
      DBMS_OUTPUT.PUT_LINE('Salari Total després:'||salaritotal);
      IF salaritotal > 10000 THEN
        raise_application_error(-20025, 'La suma del salari total de cada departament supera els 10000 Euros');
      END IF;
    END IF;
    IF UPDATING THEN
      SELECT sum(salary) INTO salaritotal FROM TREBALLA WHERE DEPARTMENT LIKE :old.DEPARTAMENT;
      DBMS_OUTPUT.PUT_LINE('Salari Total abans suma:'||salaritotal);
      salaritotal := salaritotal - :old.SALARY + :new.SALARY;
      DBMS_OUTPUT.PUT_LINE('Salari Total després:'||salaritotal);
      IF salaritotal > 10000 THEN
        raise_application_error(-20026,'La suma del salari total de cada departament supera els 10000 Euros');
      END IF;
    END IF;
  END;

END EDIT/

ERROR WHEN UPDATING:

[42000][4091] ORA-04091: table SPECIAL.TREBALLA is mutating, trigger/function may not see it ORA-06512: at "SPECIAL.CONTROLSALARIDEPT", line 14 ORA-04088: error during execution of trigger 'SPECIAL.CONTROLSALARIDEPT'

PD: Sorry, I'm very new on oracle and I need help with this trigger, I don't know even if it's right what I'm doing.... The first part of the trigger "IF INSERTING" works well, the problem is with the UPDATING...

回答1:

Try a compound trigger:

CREATE OR REPLACE TRIGGER compound_trigger_name
FOR  INSERT OR UPDATE OF salary ON treballa
COMPOUND TRIGGER

  TYPE Departments_t   IS TABLE OF treballa.department%TYPE INDEX BY varchar2(100);
  Departments          Departments_t;

     BEFORE EACH ROW IS
     BEGIN
        -- collect updated or inserted departments 
        Departments( :new.department ) := :new.department;
     END BEFORE EACH ROW;

     AFTER STATEMENT IS
        sum_sal NUMBER;
     BEGIN
      -- for each updated department check the restriction
      FOR dept IN Departments.FIRST .. Departments.LAST
      LOOP
         SELECT sum(salary) INTO sum_sal FROM treballa WHERE department = dept;
         IF sum_sal > 1000 THEN
            raise_application_error(-20123, 'The total salary for department '||dept||' cannot exceed 1000');
         END IF;
      END LOOP;
     END AFTER STATEMENT;

END compound_trigger_name;
/

========EDIT - a few question and answers ===========


Q: Why a mutating table error occurs ?
A: This is described in the documentation:
http://docs.oracle.com/cd/B28359_01/appdev.111/b28370/triggers.htm#g1699708

Trigger Restrictions on Mutating Tables
A mutating table is a table that is being modified by an UPDATE, DELETE, or INSERT statement, or a table that might be updated by the effects of a DELETE CASCADE constraint.

The session that issued the triggering statement cannot query or modify a mutating table. This restriction prevents a trigger from seeing an inconsistent set of data.

This restriction applies to all triggers that use the FOR EACH ROW clause. Views being modified in INSTEAD OF triggers are not considered mutating.

When a trigger encounters a mutating table, a run-time error occurs, the effects of the trigger body and triggering statement are rolled back, and control is returned to the user or application. (You can use compound triggers to avoid the mutating-table error. For more information, see Using Compound Triggers to Avoid Mutating-Table Error.)


Q: how to avoid a mutating table error ?
A: The documentation recommends the use of a coumpound trigger, see this: http://docs.oracle.com/cd/B28359_01/appdev.111/b28370/triggers.htm#CHDFEBFJ

Using Compound Triggers to Avoid Mutating-Table Error You can use compound triggers to avoid the mutating-table error (ORA-04091) described in Trigger Restrictions on Mutating Tables.


Q: What is a compound trigger and how does it work ?
A: This is a huge topic, please refer to the documentation here: http://docs.oracle.com/cd/B28359_01/appdev.111/b28370/triggers.htm#CIHEFGFD

In short: this is a special kind of a trigger that makes psiible to combine four types of separate triggers: BEFORE statement, BEFORE-for each row, AFTER for each row and AFTER statament into a one declaration. It makes it easier to implement some scenarious in which there is a need to pass some data from one trigger to another one. Please study the above link for more details.


Q: But what actually does "Departments( :new.department ) := :new.department; ?
A: This declaration stores a department number into an associative array.

This array is declared in a declarative part of the compound trigger:

  TYPE Departments_t   IS TABLE OF treballa.department%TYPE INDEX BY varchar2(100);
  Departments          Departments_t;

The documentation related to the compound triggers says that: http://docs.oracle.com/cd/B28359_01/appdev.111/b28370/triggers.htm#CIHJBEFE

The optional declarative part (the first part) declares variables and subprograms that timing-point sections can use. When the trigger fires, the declarative part executes before any timing-point sections execute. Variables and subprograms declared in this section have firing-statement duration.

The above means that Departments variable is initialized only once at the beginning of the whole processing, just after the trigger fires. "Firing-statement duration" means that this variable is destroyed after the trigger finishes.

This statement: Departments( :new.department ) := :new.department; stores a department number in the associative array. It is in BEFORE EACH ROW section, then it is executed for each row that is updated (or inserted) by the update/insert statement.

:new and :old are pseudorecords, more on them you can find here: http://docs.oracle.com/cd/E11882_01/appdev.112/e25519/triggers.htm#LNPLS99955
In short: :new.department retrieves a new value of department column- for a currently updated row (updated value - AFTER the update), while :old.department gives an old value of this column (BEFORE the update).

This collection is later used in the AFTER STATEMENT, when the trigers pick all updated departments (in a FOR-LOOP), for each departmens fires SELECT SUM(salary) ... and then checks if this sum is less than 1000

Consider a simple update: UPDATE treballa SET salary = salary + 10. This is a single update statement, but changes many rows at once. The order of execution of our trigger is as follows:

  1. The update statament is fired: UPDATE treballa SET salary = salary + 10
  2. The declarative section of the trigger is executed, that is: Departments variable is initialized
  3. BEFORE EACH ROW section is executed, separately for each updated row - as many times as there are rows to be updated. In this place we collect all departments from changed rows.
  4. AFTER STATEMENT section is executed. At this point the table is already updated - all rows already have new, updated salaries. We loop throught departments saved in Departments and for each one we check if sum of salaries is less or equal to 1000. If this sum is > 1000 for any of these departments, then an error is thrown, and the whole update is aborted and rolled back. Otherwise the trigger finishes, and the update is done (but you need to commit these changes anyways).

Q: What is an associative array, and why just this kind of collection is used, rather than other collections (a varray or a nested table) ?
A: PL/SQL collections are a huge topic. Follow this link to learn them: http://docs.oracle.com/cd/E11882_01/appdev.112/e25519/composites.htm#LNPLS005

In short - Associative array (or index-by table) is like a map in java (hashmap, treemap etc) - it is a set of key-value pairs, and each key is unique. You can put the same key many times to this array (with different values), but this key will be stored only once - it is unique.
I've used it to get unique set of departments.
Consider our update example again: UPDATE treballa SET salary = salary + 10 - this command touches hundreds of rows that have the same department. I don't want a collection with the same department duplicated 100 times, I need an unique set of departments, and I want to execute our query SELECT sum()... only once for each department, not 100 times. With the help of the sssociative array it is done automatically - I get unique set of deparments.