I want to create triggers for practicing PL/SQL and I sorta got stuck with these two ones, which I'm sure they're simple, but I can't get a hold of this code.
The first trigger forbids an employee to have a salary higher than the 80% of their boss (The code is incomplete because I don't know how to continue):
CREATE OR REPLACE TRIGGER MAX_SALARY
BEFORE INSERT ON EMP
FOR EACH ROW
P.BOSS EMP.JOB%TYPE := 'BOSS'
P.SALARY EMP.SAL%TYPE
BEGIN
SELECT SAL FROM EMP
WHERE
JOB != P.BOSS
...
And the second one, there must not be less than two employees per department
CREATE TRIGGER MIN_LIMIT
AFTER DELETE OR UPDATE EMPNO
EMPLOYEES NUMBER(2,0);
BEGIN
SELECT COUNT(EMPNO)INTO EMPLOYEES FROM EMP
WHERE DEPTNO = DEPT.DEPTNO;
IF EMPLOYEES < 2 THEN
DBMS_OUTPUT.PUT_LINE('There cannot be less than two employees per department');
END IF;
END;
I really don't know If I'm actually getting closer or far from it altogether...
Actually these tasks are not simple for triggers. The business logic is simple, and the SQL to execute the business logic is simple, but implementing it in triggers is hard. To understand why you need to understand how triggers work.
Triggers fire as part of a transaction, which means they are are applied to the outcome of a SQL statement such as an insert or an update. There are two types of triggers, row level and statement level triggers.
Row-level triggers fire once for every row in the result set we can reference values in the current row, which is useful for evaluating row-level rules.. But we cannot execute DML against the owning table: Oracle hurls ORA-04088 mutating table exception, because such actions violate transactional integrity.
Statement level triggers fire exactly once per statement. Consequently they are useful for enforcing table-level rules but crucially they have no access to the result set, which means they don’t know which records have been affected by the DML.
Both your business rules are table level rules, as they require the evaluation of more than one EMP record. So, can we enforce them through triggers? Let’s start with the second rule:
We could implement this with an trigger AFTER statement trigger like this:
Note this trigger uses RAISE_APPLICATION_ERROR() instead of DBMS_OUTPUT.PUT_LINE(). Raising an actual exception is always the best approach: messages can be ignored but exceptions must be handled.
The problem with this approach is that it will fail any update or delete of any employee, because the classic SCOTT.DEPT table has a record DEPTNO=40 which has no child records in EMP. So maybe we can be cool with departments which have zero employees but not with those which have just one?
This will enforce the rule. Unless of course somebody tries to insert one employee into department 40:
We can commit this. It will succeed because our trigger doesn’t fire on insert. But some other user’s update will subsequently fail. Which is obviously bobbins. So we need to include INSERT in the trigger actions.
Unfortunately now we cannot insert one employee in department 40:
We need to insert two employees in a single statement:
Note that switching existing employees to a new department has the same limitation: we have to update at least two employees in the same statement.
The other problem is that the trigger may perform badly, because we have to query the whole table after every statement. Perhaps we can do better? Yes. A compound trigger (Oracle 11g and later) allows us to track the affected records for use in a statement level AFTER trigger. Let’s see how we can use one to implement the first rule
Compound triggers are highly neat. They allow us to share program constructs across all the events of the trigger. This means we can store the values from row-level events in a collection, which we can use to drive some SQL in an statement level AFTER code..
So this trigger fires on three events. Before a SQL statement is processed we initialise a collection which uses the projection of the EMP table. The code before row stashes the pertinent values from the current row, if the employee has a manager. (Obviously the rule cannot apply to President King who has no boss). The after code loops through the stashed values, looks up the salary of the pertinent manager and evaluates the employee's new salary against their boss's salary.
This code will check every employee if the update is universal, for instance when everybody gets a 20% pay rise...
But if we only update a subset of the EMP table it only checks the boss records it needs to:
This makes it more efficient than the previous trigger. We could re-write trigger MIN_LIMIT as a compound trigger; that is left as an exercise for the reader :)
Likewise, each trigger fails as soon as a single violating row is found:
It would be possible to evaluate all affected rows, stash the violating row(s) in another collection then display all the rows in the collection. Another exercise for the reader.
Finally, note that having two triggers fire on the same event on the same table is not good practice. It's generally better (more efficient, easier to debug) to have one trigger which does everything.
An after thought. What happens to Rule #1 if one session increases the salary of an employee whilst simultaneously another session decreases the salary of the boss? The trigger will pass both updates but we can end up with a violation of the rule. This is an inevitable consequence of the way triggers work with Oracle's read-commit transaction consistency. There is no way to avoid it except by employing a pessimistic locking strategy and pre-emptively locking all the rows which might be affected by a change. That may not scale and is definitely hard to implement using pure SQL: it needs stored procedures. This is another reason why triggers are not good for enforcing business rules.
That is unfortunate. Oracle 10g has been obsolete for almost a decade now. Even 11g is deprecated. However, if you really have no option but to stick with 10g you have a couple of options.
The first is to grind through the whole table, doing the lookups of each boss for every employee. This is just about bearable for a toy table such as EMP but likely to be a performance disaster in real life.
The better option is to fake compound triggers using the same workaround we all used to apply: write a package. We rely on global variables - collections - to maintain state across calls to packaged procedures, and have different triggers to make those calls. Basically you need one procedure call for each trigger and one trigger for each step in the compound trigger. @JustinCave posted an example of how to do this on another question; it should be simple to translate my code above to his template.
Please handle these kind of validations/business logic at application or at DB level using procedures/functions instead of using triggers which most of the times slows down the DML operations/statements on which the triggers are based.
If you handle business logic at application or procedure level then, the DB server will have to execute only DML statements; it does not have to execute TRIGGER-executing trigger involves handling exceptions; prior to that DML statement will place a lock on the table on which DML (except for INSERT statement-Exclusive shared lock) is being executed until TRIGGER is executed.