Trigger to enforce constraint that employee never change department - oracle

Suppose I have following schema :
DEPARTMENT (DepartmentName, BudgetCode, OfficeNumber, Phone)
EMPLOYEE (EmployeeNumber, FirstName, LastName, Department, Phone, Email)
Now I need to write a trigger that enforce the constraint that an employee can never change his or her department.
CREATE OR REPLACE TRIGGER department_fixed
BEFORE UPDATE ON EMPLOYEE
FOR EACH ROW
WHEN (old.Department is not null)
BEGIN
dbms_output.put('You can not change department');
END;
/
Is this right way to write this oracle trigger or am missing something ?
Also please help me to write a trigger to allow the deletion of a department if it only has one employee. Assign the last employee to the Human Resources department.

You're close. What this trigger is going to do is for every update of the EMPLOYEE table, it's going to check the original department and if it's not null, it will print something to the screen. I'm assuming you're allowing updates for other fields, right? So my trigger would look like this:
CREATE OR REPLACE TRIGGER EMPLOYEE_BUR
BEFORE UPDATE ON EMPLOYEE
FOR EACH ROW
DECLARE
DEPT_EXCEPTION EXCEPTION; --Declare the exception
PRAGMA EXCEPTION_INIT(DEPT_EXCEPTION, -20002);
L_COUNT NUMBER
BEGIN
SELECT COUNT(*) INTO L_COUNT FROM DEPARTMENT D
WHERE D.DEPARTMENT=:OLD.DEPARTMENT;
IF (:OLD.DEPARTMENT <> :NEW.DEPARTMENT) AND (L_COUNT > 0) THEN
RAISE DEPT_EXCEPTION;
END IF;
END;
.. and then I handle the DEPT_EXCEPTION in my calling procedure. You must either raise an exception -OR- do something with :NEW.DEPARTMENT or else the trigger will finish and the update statement will complete. The "EMPLOYEE_BUR" name is more for best practices, this way you know what it is. "BUR" - "Before Update for each Row"
For the second part, you could have a trigger on DEPARTMENT that fires before the delete to confirm deletion, then one after to reassign the employee:
CREATE OR REPLACE TRIGGER DEPARTMENT_BDR
BEFORE DELETE ON DEPARTMENT
FOR EACH ROW
DECLARE
DEPT_COUNT_EXCEPTION EXCEPTION;
PRAGMA EXCEPTION_INIT(DEPT_COUNT_EXCEPTION, -20003);
L_COUNT NUMBER;
BEGIN
SELECT COUNT(*) INTO L_COUNT FROM EMPLOYEE E
WHERE E.DEPARTMENT=:OLD.DEPARTMENT;
IF (L_COUNT > 1 ) THEN
RAISE DEPT_COUNT_EXCEPTION;
END IF;
END;
Then...
CREATE OR REPLACE TRIGGER DEPARTMENT_ADR
AFTER DELETE ON DEPARTMENT
FOR EACH ROW
BEGIN
UPDATE EMPLOYEE
SET DEPARTMENT='HR'
WHERE EMPLOYEE=:OLD.EMPLOYEE;
END;
So this way, if you're deleting a Department with more than one employee, trigger 2 fires and raises an exception so trigger 3 will not. If trigger 2 completes fine, then trigger 3 will fire which re-assigns the employee. If the delete statement completed, then the update will be allowed since there is no former department.

Related

Insert trigger if record exists - Oracle

I have tables:
DEPARTMETS with dep_id(number), dep_name(varchar), manager_id(number) fields.
EMPLOYEES with employee_id(number), name(varchar), salary(number), manager_id(number)
I want to create a trigger which it is responsible for check if exists manager_id into table DEPARTMENTS when data is inserted or updated to table EMPLOYEES
Trigger could be something like:
create or update trigger manager_exists
before insert or update on employees
for each row
begin
if exists **new id** into Departments then
INSERT DATA IN EMPLOYEES
else
"Error: MANAGER_ID doesnt exists in Departments"
end if;
end manager_exists;
But I can't figure out how to create this trigger.
Note:I need it to be a trigger please. HELP!
This is how I understood the question:
create or replace trigger manager_exists
before insert or update on employees
for each row
declare
l_mgr number;
begin
select 1
into l_mgr
from dual
where exists (select null
from departments d
where d.manager_id = :new.manager_id
);
exception
when no_data_found then
raise_application_error(-20000, 'That manager does not exist in DEPARTMENTS');
end manager_exists;
In other words:
check whether MANAGER_ID you're trying to insert into EMPLOYEES table exists in DEPARTMENTS table
though, that doesn't make much sense to me; I'd say that you should check it vice versa - check whether MANAGER_ID you're trying to insert into DEPARTMENTS exists in EMPLOYEES table ... but that's not what you wrote (or I misunderstood what you said)
if so, fine, don't do anything (in a trigger); insert or update statement which caused the trigger to fire will do the job
if not, raise an error

Trying to create a trigger to check if there's more than 1 president in my database

I'm trying to find if there's more than 1 president in my database with a trigger and if yes, raise an error, I'm using hr, the table employees and I have to use the job_id to find it. Here's what my code looks like. Thanks!
CREATE OR REPLACE TRIGGER check_pres
BEFORE INSERT OR DELETE OR UPDATE ON employees
FOR EACH ROW
BEGIN
IF ((employees.job_id = 'AD_PRES') > 1)
THEN RAISE_APPLICATION_ERROR(-12345, 'More than one President in database.');
END IF;
END;
You should use Statement Level Trigger instead of Row Level Trigger by removing FOR EACH ROW expression
CREATE OR REPLACE TRIGGER check_pres
BEFORE INSERT OR DELETE OR UPDATE ON employees
DECLARE
v_cnt int;
BEGIN
SELECT COUNT(*) INTO v_cnt FROM employees WHERE job_id = 'AD_PRES';
IF ( v_cnt > 1 ) THEN
RAISE_APPLICATION_ERROR(-20345, 'More than one President in database.');
END IF;
END;
otherwise you'd get mutating error while getting the count value. Btw, the first argument value for RAISE_APPLICATION_ERROR should be between -20999 and -20000

How to write triggers to enforce business rules?

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...
which I'm sure they're simple
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:
there must not be less than two employees per department
We could implement this with an trigger AFTER statement trigger like this:
CREATE or replace TRIGGER MIN_LIMIT
AFTER DELETE OR UPDATE on EMP
declare
EMPLOYEES pls_integer;
BEGIN
for i in ( select * from dept) loop
SELECT COUNT(EMPNO) INTO EMPLOYEES
FROM EMP
where i.DEPTNO = EMP.DEPTNO;
IF EMPLOYEES < 2 THEN
raise_application_error(-20042, 'problem with dept #' || i.DEPTNO || '. There cannot be less than two employees per department');
END IF;
end loop;
END;
/
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?
CREATE or replace TRIGGER MIN_LIMIT
AFTER DELETE OR UPDATE on EMP
declare
EMPLOYEES pls_integer;
BEGIN
for i in ( select deptno, count(*) as emp_cnt
from emp
group by deptno having count(*) < 2
) loop
raise_application_error(-20042, 'problem with dept #' || i.DEPTNO || '. There cannot be less than two employees per department');
end loop;
END;
/
This will enforce the rule. Unless of course somebody tries to insert one employee into department 40:
insert into emp
values( 2323, 'APC', ‘DEVELOPER', 7839, sysdate, 4200, null, 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.
CREATE or replace TRIGGER MIN_LIMIT
AFTER INSERT or DELETE OR UPDATE on EMP
declare
EMPLOYEES pls_integer;
BEGIN
for i in ( select deptno, count(*) as emp_cnt
from emp
group by deptno having count(*) < 2
) loop
raise_application_error(-20042, 'problem with dept #' || i.DEPTNO || '. There cannot be less than two employees per department');
end loop;
END;
/
Unfortunately now we cannot insert one employee in department 40:
ORA-20042: problem with dept #40. There cannot be less than two employees per department
ORA-06512: at "APC.MIN_LIMIT", line 10
ORA-06512: at "SYS.DBMS_SQL", line 1721
We need to insert two employees in a single statement:
insert into emp
select 2323, 'APC', 'DEVELOPER', 7839, sysdate, 4200, null, 40 from dual union all
select 2324, 'ANGEL', 'DEVELOPER', 7839, sysdate, 4200, null, 40 from dual
/
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
No employee can have a salary higher than the 80% of their boss
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.
CREATE OR REPLACE TRIGGER MAX_SALARY
FOR INSERT OR UPDATE ON EMP
COMPOUND TRIGGER
type emp_array is table of emp%rowtype index by simple_integer;
emps_nt emp_array ;
v_idx simple_integer := 0;
BEFORE STATEMENT IS
BEGIN
emps_nt := new emp_array();
END BEFORE STATEMENT;
BEFORE EACH ROW IS
BEGIN
v_idx := v_idx + 1;
if :new.mgr is not null then
emps_nt(v_idx).empno := :new.empno;
emps_nt(v_idx).mgr := :new.mgr;
emps_nt(v_idx).sal := :new.sal;
end if;
END BEFORE EACH ROW;
AFTER EACH ROW IS
BEGIN
null;
END AFTER EACH ROW;
AFTER STATEMENT IS
mgr_sal emp.sal%type;
BEGIN
for i in emps_nt.first() .. emps_nt.last() loop
select sal into mgr_sal
from emp
where emp.empno = emps_nt(i).mgr;
if emps_nt(i).sal > (mgr_sal * 0.8) then
raise_application_error(-20024, 'salary of empno ' || emps_nt(i).empno || ' is too high!');
end if;
end loop;
END AFTER STATEMENT;
END;
/
This code will check every employee if the update is universal, for instance when everybody gets a 20% pay rise...
update emp
set sal = sal * 1.2
/
But if we only update a subset of the EMP table it only checks the boss records it needs to:
update emp set sal = sal * 1.2
where deptno = 20
/
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:
ORA-20024: salary of empno 7902 is too high!
ORA-06512: at "APC.MAX_SALARY", line 36
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.
I'm using Oracle10g
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.

What is the proper way to create an update trigger with a variable getting value from the same table?

I'm creating a update trigger where an employee can never have a salary that is greater than the president's. However I need to subquery the president's salary for comparison and the "new" updated employee's salary.
I originally had the the subquery using from the from employees table but had to make a new table because of the mutating table problem. I don't think creating a new table is plausible solution for a real implementation.
Is there a way I can save the president's salary without creating a new table?
CREATE OR REPLACE TRIGGER prevent_salary
BEFORE UPDATE ON employees
FOR EACH ROW
declare pres_sal number(8,2);
BEGIN
select salary into pres_sal from employees_salary where job_id='AD_PRES';--employees_salary was employees but that gives mutating error
IF (:new.salary > pres_sal)
THEN UPDATE employees
SET salary = :old.salary
WHERE employee_id = :old.employee_id;
END IF;
END;
One way to do it is to save off the president's salary in a BEFORE STATEMENT trigger and then use that in the FOR EACH ROW trigger.
"Compound Triggers", which have been around at least since version 11.1, offer a nice way to do that all in one place.
Here is an example:
CREATE OR REPLACE TRIGGER prevent_salary
FOR UPDATE OF salary ON employees
COMPOUND TRIGGER
pres_sal NUMBER;
BEFORE STATEMENT IS
BEGIN
select salary
into pres_sal
from employees
where job_id='AD_PRES';
END BEFORE STATEMENT;
BEFORE EACH ROW IS
BEGIN
:new.salary := least(:new.salary, pres_sal);
END BEFORE EACH ROW;
END prevent_salary;
You may try this :
CREATE OR REPLACE TRIGGER prevent_salary
BEFORE UPDATE ON employees
FOR EACH ROW
declare pres_sal number(8,2);
BEGIN
select salary into pres_sal from employees_salary where job_id='AD_PRES';--employees_salary was employees but that gives mutating error
IF (:new.salary > pres_sal)
Raise_Application_Error (-20101, 'An employee''s salary couldn''t exceed president''s !');
END IF;
END;

PL/SQL Triggers

I am using these tables:
flights (flno, origin, destination, distance, departs, arrives, price)
aircraft (aid, aname, crusingrange)
employees (eid, ename, salary)
certified (eid,aid)
and I need to create a trigger that displays a warning when inserting an employee with "666" anywhere in his/her name.
This is what I came up with so far; I am lost with the rest of it.
set serveroutput on
create or replace trigger emp_warning
before insert
on employees
for each row
declare
v_name;
begin
select e.ename into v_ename
from employees e
A trigger cannot "display a warning"; a trigger can raise an exception.
In the context of the body of a before insert for each row trigger, the value being supplied for the column is available from :NEW.columname
For example:
BEGIN
IF :NEW.ename LIKE '%666%' THEN
RAISE_APPLICATION_ERROR(-20000, 'ename contains ''666''.');
END IF;
END;
It's not mandatory that you use the RAISE_APPLICATION_ERROR. You could emit some line(s) using DBMS_OUTPUT.PUT_LINE... the line could include whatever text you wanted, including the word "warning". But this isn't really a display of a warning.
Use a check constraint instead of a trigger:
alter table empoloyees modify ename check (ename not like '%666%');
I finally figured it out thanks for you help. This my answer to the trigger question.
set serveroutput on
create or replace trigger name_warning
before insert on employees
for each row
begin
if :new.ename like '%666%' then
dbms_output.put_line('Warning employees name contains 666');
end if;
end;
/

Resources