PL/SQL Triggers - sql

I am trying to make a Library Information System. I have a table called Borrower(borrower_id: number, name: varchar2(30), status: varchar2(20)). 'status' can be either 'student' or 'faculty'.
I have a restriction that a maximum of 2 books can be issued to a student at any point of time, and 3 to a faculty. How do I implement it using triggers?
This is a homework question. But I've tried hard to come up with some logic. I am new to SQL so this might be easy for you lot but not for me.
I am new to stackexchange, so sorry if I've violated some rules/practices.

I expect that you would maintain a count on the borrower table of the number of books borrowed, and modify it via a trigger when a book is borrowed and when it is returned. Presumably you also have a table for the books being borrowed by the user, and the trigger would be placed on that take.
A constraint on the books_borrowed column could raise an error if the count of borrowed books exceeds 2.

This is a pretty old question, but I found it very useful as I'm also a PL/SQL beginner. There are two approaches to solve the problem and the one you want to use depends on the Oracle DB version.
For older version use a combination of a trigger and a package as below.
CREATE OR REPLACE TRIGGER trg_borrower
BEFORE INSERT OR UPDATE ON borrower
FOR EACH ROW
DECLARE
v_count NUMBER := 0;
BEGIN
v_count := borrower_pkg.count_rows(:NEW.borrower_id, :NEW.name, :NEW.status);
IF :NEW.status = 'student' AND v_count = 2 THEN
RAISE_APPLICATION_ERROR(-20000, 'Error - student');
ELSIF :NEW.status = 'faculty' AND v_count = 3 THEN
RAISE_APPLICATION_ERROR(-20001, 'Error - faculty');
END IF;
END;
/
CREATE OR REPLACE PACKAGE borrower_pkg AS
FUNCTION count_rows(p_id IN borrower.borrower_id%TYPE,
p_name IN borrower.NAME%TYPE,
p_status IN borrower.status%TYPE) RETURN NUMBER;
END;
/
CREATE OR REPLACE PACKAGE BODY borrower_pkg AS
FUNCTION count_rows(p_id IN borrower.borrower_id%TYPE,
p_name IN borrower.NAME%TYPE,
p_status IN borrower.status%TYPE) RETURN NUMBER AS
v_count NUMBER := 0;
BEGIN
SELECT COUNT(*) INTO v_count
FROM borrower
WHERE borrower_id = p_id AND NAME = p_name AND status = p_status;
RETURN v_count;
END count_rows;
END borrower_pkg;
/
For Oracle 10g and above you can use a compound trigger.
CREATE OR REPLACE TRIGGER trg_borrower_comp
FOR INSERT OR UPDATE ON borrower
COMPOUND TRIGGER
CURSOR c_borrower IS
SELECT b1.borrower_id
FROM borrower b1
WHERE EXISTS (SELECT 'x'
FROM borrower b2
WHERE b2.status = 'student' AND b1.borrower_id = b2.borrower_id
GROUP BY borrower_id HAVING COUNT(*) = 2)
OR
EXISTS (SELECT 'x'
FROM borrower b3
WHERE status = 'faculty'AND b1.borrower_id = b3.borrower_id
GROUP BY borrower_id HAVING COUNT(*) = 3);
TYPE t_borrower_count IS TABLE OF borrower.borrower_id%type;
v_borrower_count t_borrower_count;
BEFORE STATEMENT IS
BEGIN
OPEN c_borrower;
FETCH c_borrower BULK COLLECT INTO v_borrower_count;
CLOSE c_borrower;
END BEFORE STATEMENT;
BEFORE EACH ROW IS
BEGIN
IF :NEW.borrower_id MEMBER OF v_borrower_count THEN
RAISE_APPLICATION_ERROR(-20000, 'Error - ' || :NEW.status);
END IF;
END BEFORE EACH ROW;
END;

Related

sql insert procedure: insert values into table and control if value is existing in another table

I have little problem. I am trying to insert value into table. This is working. But I would like to control if value id_trainer is existing in another table. I want this -> execute insertClub(1, 5, 'someName'); -> and if id_trainer 5 not exists in table Trainer, procedure gives me message about this. (I tried to translate it into eng. lng., so you can find some gramm. mistakes)
create or replace procedure insertClub
(id_club in number, id_trainer in number, clubName in varchar2)
is
begin
declare counter number;
select count(*) into counter from trianer tr where tr.id_trainer = id_trainer;
if counter = 0 then
DBMS_OUTPUT.PUT_LINE('Trainer with this ID not exists');
end if;
insert into club values(id_club, id_trainer, clubName);
exception
when dup_val_on_index then
DBMS_OUTPUT.PUT_LINE('Dup ID');
end;
/
There is some error in the procedure. Please run below code to create procedure:
create or replace procedure insertClub
(id_club in number, id_trainer in number, clubName in varchar2)
is
counter number;
begin
select count(*) into counter from trianer tr where tr.id_trainer = id_trainer;
if counter = 0 then
DBMS_OUTPUT.PUT_LINE('Trainer with this ID not exists');
end if;
insert into club values(id_club, id_trainer, clubName);
exception
when dup_val_on_index then
DBMS_OUTPUT.PUT_LINE('Dup ID');
end;
/

How to avoid "table mutating" errors

I have an trigger that needs to read from a table after deleting a row. Essentially, I need to count up the remaining rows that are similar to the current row, and if that count is zero, update a field elsewhere.
After two days of hammering around, I haven't been able to figure out how to restructure my thought process to allow me to do this. Here is an example:
CREATE OR REPLACE TRIGGER Di_PatMustBeWell
AFTER DELETE
ON Diagnosis
FOR EACH ROW
Declare
--PRAGMA AUTONOMOUS_TRANSACTION;
NumDiseases Number;
BEGIN
SELECT NUMDISEASES INTO Numdiseases
FROM DiagnosisCount
where Di_Patient = :OLD.Di_Patient;
IF( NumDiseases != 1 ) THEN
UPDATE Patient SET Pat_Sick = 0 WHERE Pat_Person = :OLD.Di_Patient;
END IF;
END;
/
Short answer - no trigger, no mutating.
Yow can use the trigger with pragma autonomous_transaction for counting of remaining diagnoses for certain patient, but it's is not recommended way to do this.
Better you create new function or procedure to implement your logic on deleted diagnosis. Something like this:
create table Diagnosis as select 456 idDiseases, 123 di_patient from dual;
/
create table diagnosisCount as select 1 numDiseases, 123 di_patient from dual;
/
create table Patient as select 123 Pat_Person, 1 Pat_Sick from dual;
/
drop trigger di_patmustbewell;
create or replace function deleteDiagnosis(idDiseases number) return number is
rows_ number;
di_patient number;
Numdiseases number;
begin
<<del>> begin
delete Diagnosis where IdDiseases = deleteDiagnosis.IdDiseases
returning Diagnosis.di_patient into deleteDiagnosis.di_patient
;
rows_ := sql%rowcount;
if rows_ != 1 then raise too_many_rows; end if;
end del;
select count(1) into deleteDiagnosis.numDiseases from Diagnosis where Di_Patient = deleteDiagnosis.di_patient;
if deleteDiagnosis.numdiseases = 0 then <<upd>> begin
update Patient set Pat_Sick = 0 where Pat_Person = deleteDiagnosis.di_patient;
exception when others then
dbms_output.put_line('Cannot update Patient di_patient='||di_patient);
raise;
end upd; end if;
return rows_;
end;
/
show errors
declare rows_ number := deleteDiagnosis(456);
begin dbms_output.put_line('deleted '||rows_||' rows'); end;
/
deleted 1 rows
select * from Patient;
PAT_PERSON PAT_SICK
---------- ----------
123 0
An alternative solution, if you prefer (or must) to use a trigger in your application - declare internal function returning count of patient's diagnoses in the trigger body:
create or replace trigger di_patmustbewell
after delete on diagnosis for each row
declare
numdiseases number;
function getNumDiagnosis (di_patient number) return number is
ret number;
pragma autonomous_transaction;
begin
select count(1) into ret from diagnosis where di_patient = getNumDiagnosis.di_patient;
return ret;
end getNumDiagnosis;
begin
numDiseases := getNumDiagnosis(:old.di_patient);
if(numdiseases = 0) then
update patient set pat_sick = 0 where pat_person = :old.di_patient;
end if;
end;
/
show errors;
Trigger DI_PATMUSTBEWELL compiled
Hope it helps you a bit.
You can create a COMPOUND trigger for such cases:
create or replace TRIGGER Di_PatMustBeWell
FOR DELETE ON Diagnosis
COMPOUND TRIGGER
TYPE Di_Patient_Table_type IS TABLE OF DiagnosisCount.Di_Patient%TYPE;
Di_Patient_Table Di_Patient_Table_type;
BEFORE STATEMENT IS
BEGIN
Di_Patient_Table := Di_Patient_Table_type();
END BEFORE STATEMENT;
BEFORE EACH ROW IS
BEGIN
Di_Patient_Table.EXTEND;
Di_Patient_Table(Di_Patient_Table.LAST) := :OLD.Di_Patient;
END BEFORE EACH ROW;
AFTER STATEMENT IS
BEGIN
FOR i IN Di_Patient_Table.FIRST..Di_Patient_Table.LAST LOOP
SELECT NUMDISEASES INTO Numdiseases
FROM DiagnosisCount
where Di_Patient = Di_Patient_Table(i);
IF NumDiseases != 1 THEN
UPDATE Patient SET Pat_Sick = 0 WHERE Pat_Person = Di_Patient_Table(i);
END IF;
END LOOP;
Di_Patient_Table.DELETE;
END AFTER STATEMENT;
END;
/

Mutating Trigger - SQL

Another one of those mutating trigger errors without an idea of how to fix it. The function counts the number of reviews given and the trigger displays the number of review records:
CREATE OR REPLACE FUNCTION func_no_of_reviews_for_venue (in_venue_id reviews.venue_id%TYPE)
RETURN NUMBER IS
vn_no_of_reviews reviews.review_id%TYPE := 0;
BEGIN
SELECT COUNT(*)
INTO vn_no_of_reviews
FROM reviews;
RETURN vn_no_of_reviews;
END func_no_of_reviews_for_venue;
/
SHOW ERRORS;
-- trigger to get a venue's average rating when a new review has been added
CREATE OR REPLACE TRIGGER trig_get_venue_avg_rating
AFTER INSERT OR UPDATE OF rating ON reviews
FOR EACH ROW
WHEN (NEW.rating IS NOT NULL)
DECLARE
vn_no_reviews NUMBER(6);
vn_avg NUMBER(2,1);
BEGIN
vn_no_reviews := func_no_of_reviews_for_venue(:NEW.venue_id);
DBMS_OUTPUT.PUT_LINE ('Number of reviews: ' || vn_no_reviews);
END trig_age_ck;
/

Trigger that restricts by date

Im trying to create a trigger that restricts the amount a reader can read in a given month.
CREATE OR REPLACE trigger Readings_Limit
Before update or insert on reading
for each row declare
readingcount integer;
max_read integer := 5;
Begin
Select count(*) into readingcount
from (select *
from Reading
where to_char(DateRead, 'YYYY-MM') = to_char(DateRead, 'YYYY-MM'))
where employeeid = :new.employeeid;
if :old.employeeid = :new.employeeid then
return;
else
if readingcount >= max_read then
raise_application_error (-20000, 'An Employee can only read 5 per month');
end if;
end if;
end;
This restricts the reader to 5 max in total no matter the month, i can't seem to get it to be 5 max each month. any ideas greatly appreciated!
Try to rewrite your trigger like this:
CREATE OR REPLACE trigger Readings_Limit
Before update or insert on reading
for each row
declare
readingcount integer;
max_read integer := 5;
Begin
Select count(*) into readingcount
from Reading
where DateRead between trunc(sysdate,'MM') and last_day(sysdate)
and employeeid = :new.employeeid;
if :old.employeeid = :new.employeeid then
return;
else
if readingcount >= max_read then
raise_application_error (-20000, 'An Employee can only read 5 per month');
end if;
end if;
end;
You add actual month into yout select and you avoid unnecessary date conversion.
I don't understand the condition
if :old.employeeid = :new.employeeid then
Does it mean, the trigger should not fire on updates? In this case it is better to make trigger only for insert or use clause if inserting then...
In order to properly create this validation using a trigger a procedure should be created to obtain user-specified locks so the validation can be correctly serialized in a multi-user environment.
PROCEDURE request_lock
(p_lockname IN VARCHAR2
,p_lockmode IN INTEGER DEFAULT dbms_lock.x_mode
,p_timeout IN INTEGER DEFAULT 60
,p_release_on_commit IN BOOLEAN DEFAULT TRUE
,p_expiration_secs IN INTEGER DEFAULT 600)
IS
-- dbms_lock.allocate_unique issues implicit commit, so place in its own
-- transaction so it does not affect the caller
PRAGMA AUTONOMOUS_TRANSACTION;
l_lockhandle VARCHAR2(128);
l_return NUMBER;
BEGIN
dbms_lock.allocate_unique
(lockname => p_lockname
,lockhandle => p_lockhandle
,expiration_secs => p_expiration_secs);
l_return := dbms_lock.request
(lockhandle => l_lockhandle
,lockmode => p_lockmode
,timeout => p_timeout
,release_on_commit => p_release_on_commit);
IF (l_return not in (0,4)) THEN
raise_application_error(-20001, 'dbms_lock.request Return Value ' || l_return);
END IF;
-- Must COMMIT an autonomous transaction
COMMIT;
END request_lock;
To have the least impact on scalability the serialization should be done at the finest level, which for this constraint is per employeeid and month. Types may be used in order to create variables to store this information for each row before the constraint is checked after the statement has completed. These types can be either defined in the database or (from Oracle 12c) in package specifications.
CREATE OR REPLACE TYPE reading_rec
AS OBJECT
(employeeid NUMBER(10) -- Note should match the datatype of reading.employeeid
,dateread DATE);
CREATE OR REPLACE TYPE readings_tbl
AS TABLE OF reading_rec;
The procedure and types can then be used in a compound trigger (assuming at least Oracle 11, this will need to be split into individual triggers in earlier versions)
CREATE OR REPLACE TRIGGER too_many_readings
FOR INSERT OR UPDATE ON reading
COMPOUND TRIGGER
-- Table to hold identifiers of inserted/updated readings
g_readings readings_tbl;
BEFORE STATEMENT
IS
BEGIN
-- Reset the internal readings table
g_readings := readings_tbl();
END BEFORE STATEMENT;
AFTER EACH ROW
IS
BEGIN
-- Store the inserted/updated readings
IF ( INSERTING
OR :new.employeeid <> :old.employeeid
OR :new.dateread <> :old.dateread)
THEN
g_readings.EXTEND;
g_readings(g_readings.LAST) := reading_rec(:new.employeeid, :new.dateread);
END IF;
END AFTER EACH ROW;
AFTER STATEMENT
IS
CURSOR csr_readings
IS
SELECT DISTINCT
employeeid
,trunc(dateread,'MM') monthread
FROM TABLE(g_readings)
ORDER BY employeeid
,trunc(dateread,'MM');
CURSOR csr_constraint_violations
(p_employeeid reading.employeeid%TYPE
,p_monthread reading.dateread%TYPE)
IS
SELECT count(*) readings
FROM reading rdg
WHERE rdg.employeeid = p_employeeid
AND trunc(rdg.dateread, 'MM') = p_monthread
HAVING count(*) > 5;
r_constraint_violation csr_constraint_violations%ROWTYPE;
BEGIN
-- Check if for any inserted/updated readings there exists more than
-- 5 readings for the same employee in the same month. Serialise the
-- constraint for each employeeid so concurrent transactions do not
-- affect each other
FOR r_reading IN csr_readings LOOP
request_lock('TOO_MANY_READINGS_'
|| r_reading.employeeid
|| '_' || to_char(r_reading.monthread, 'YYYYMM'));
OPEN csr_constraint_violations(r_reading.employeeid, r_reading.monthread);
FETCH csr_constraint_violations INTO r_constraint_violation;
IF csr_constraint_violations%FOUND THEN
CLOSE csr_constraint_violations;
raise_application_error(-20001, 'Employee ' || r_reading.employeeid
|| ' now has ' || r_constraint_violation.readings
|| ' in ' || to_char(r_reading.monthread, 'FMMonth YYYY'));
ELSE
CLOSE csr_constraint_violations;
END IF;
END LOOP;
END AFTER STATEMENT;
END;
You need to set the month you are looking at, so if you are considering the current month, make the inner query read like this:
( select * from Reading
where to_char(DateRead,'YYYY-MM') = to_char(DateRead,'YYYY-MM')
and to_char(sysdate,'YYYY-MM') = to_char(DateRead,'YYYY-MM'))
that way it will always compare to the current month and should move as your date moves.

Oracle Procedure - Return all rows

So I have several tables and I've created different roles to go with different users so that each can access a portion of the tables.
Right now, whenever I try to SELECT * FROM yaser.enrol; with a coordinator who is meant to see everything, I get the error numeric or value error: character to number conversion error which points to the lines where i'm querying the employee_no to determine the employee role.
Theres 4 overall types of users: student, tutor, lecturer, coordinator.
EDIT ** Added all code.
-- Create policy function to be called when ‘ENROL’ table is accessed under user Yaser.
create or replace function f_policy_enrol (schema in varchar2, tab in varchar2)
return varchar2
as
v_emp_no varchar2(10);
v_student_no varchar2(10);
v_tutor_emp_no varchar2(10);
v_lecturer_emp_no varchar2(10);
v_coord_emp_no varchar2(10);
is_tutor number:=0;
is_lecturer number:=0;
is_coordinator number:=0;
is_student number:=0;
is_employee number:=0;
v_program_code varchar2(10);
v_user varchar2(100);
out_string varchar2(400) default '1=2 ';
-- The return value will be out_string. '1=2' means 'Nothing to access'.
begin
-- get session user
v_user := lower(sys_context('userenv','session_user'));
-- Is the user a student?
begin
SELECT student_no INTO v_student_no FROM student WHERE lower(student_no) = v_user;
is_student:=1;
exception
when no_data_found then
v_student_no := 0;
end;
-- Is the user an employee?
begin
SELECT emp_no INTO v_emp_no FROM employee WHERE lower(emp_no) = v_user;
is_employee:=1;
exception
when no_data_found then
v_emp_no := 0;
end;
-- Query the employee number to determine role.
-- If Tutor.
SELECT MAX(tutor_emp_no) INTO v_tutor_emp_no FROM tutorial WHERE lower(tutor_emp_no) = v_user;
-- If Lecturer.
SELECT MAX(course_coord_emp_no) INTO v_lecturer_emp_no FROM course WHERE lower(course_coord_emp_no) = v_user;
-- If Coordinator.
SELECT MAX(prog_coord_emp_no) into v_coord_emp_no FROM program WHERE lower(prog_coord_emp_no) = v_user;
-- Get role of the employee if the user is an employee.
if v_emp_no != 0 and v_tutor_emp_no is NOT NULL then
-- Employee is a Tutor.
is_tutor := 1;
elsif v_emp_no != 0 and v_lecturer_emp_no is NOT NULL then
-- Employee is Lecturer.
is_lecturer := 1;
elsif v_emp_no != 0 and v_coord_emp_no is NOT NULL then
-- Employee is Coordinator.
is_coordinator := 1;
end if;
-- Create the string to be used as the WHERE clause.
if is_student = 1 then
-- Students are allowed to see their orders only.
out_string := out_string||'or student_no = '''||v_student_no||''' ';
end if;
if is_tutor = 1 then
-- Tutors are allowed to see enrolments of students that they tutor.
---- out_string := out_string||'or student_no in (select student_no from tutorial where tutor_emp_no = '||v_tutor_emp_no||') ';
---- NOT WORKING.
out_string := out_string||'or student_no in (select student_no from tutorial where lower(tutor_emp_no) = v_tutor_emp_no) ';
end if;
if is_coordinator = 1 then
-- The coordinator is allowed to see all records in ENROL (WHERE 1=1 or anything) means all rows.
out_string := out_string||'or 1=1 ';
end if;
return out_string;
end;
/
These are the tables i'm referencing:
CREATE TABLE course
(
course_code varchar(10),
course_title varchar(50),
course_coord_emp_no varchar(10),
primary key (course_code)
);
And - all employees:
CREATE TABLE employee
(
emp_no varchar(10),
name varchar(100)
);
All other tables are basically the same - VARCHARS
Any help would be good! :)
Yaser
The question has evolved a lot and much of this answer was about a missing end if that wasn't relevant to your actual code. To go actually go back to your original question, using or 1=1 as a catch-all is OK even though the other branch of the if if comparing strings - it makes no difference at all. If you really did want to compare strings you can do the same thing:
'or ''x''=''x'' '
... or
'or v_user=v_user '
But you can't compare empty strings as you seem to show in a comment. An empty string, '', is the same as null in Oracle , and you can't equate anything to null, even itself. (The previous check would fail if v_user was null, for the same reason). So another possibility would be:
'or null is null '
Note that the comparator here is is, not =.
None of which addresses why you get the VPD error, as all of those are equivalent really - they all always evaluate to true and it doesn't matter which you use. Anything that effectively ends up as or true would work exactly the same; just a shame that Oracle SQL doesn't understand booleans like that. The datatypes being compared in any other clauses are irrelevant to this clause.
You need to see what the function is actually returning in both cases, verify it's what you expect, and verify that it works as a restriction when you query the table the VPD is against directly.