How to get table fields from which was called a trigger with Oracle 10g? - oracle

I want to calculate the average of values ​​in a column of my table (PRA_COEFF) (RAPPORT_VISITE) for a field (PRA_NUM) given, when I add or change a row of it. Then I want to save this value in another table (PRACTITIONER) to the row where PRA_NUM worth PRA_NUM given above.
CREATE TABLE "RAPPORT_VISITE"
(
"RAP_NUM" NUMBER (10,0),
"PRA_NUM" NUMBER (10,0),
"PRA_COEFF" NUMBER (10,0),
)
CREATE TABLE "PRATICIEN"
(
"PRA_NUM" NUMBER (10,0),
"PRA_COEFCONF" NUMBER
)
The trigger is called when adding or Modification RAPPORT_VISITE the table. I tried like this, but I can not retrieve the row affected by the trigger, and thus PRA_NUM, that I need to read.
create or replace TRIGGER UDPATE_PRAT_COEFCONF
AFTER INSERT or UPDATE ON RAPPORT_VISITE
DECLARE
somme NUMBER;
nb NUMBER;
moyenne NUMBER;
rapport NUMBER;
pra_id NUMBER;
BEGIN
/*SELECT MAX(RAP_NUM) INTO rapport FROM RAPPORT_VISITE; // Not want I need in case where I modify a row... */
SELECT PRA_NUM INTO pra_id FROM RAPPORT_VISITE WHERE RAP_NUM=rapport;
SELECT SUM(PRA_COEFF) INTO somme FROM RAPPORT_VISITE WHERE PRA_NUM=pra_id;
SELECT COUNT(*) INTO nb FROM RAPPORT_VISITE WHERE PRA_NUM=pra_id;
IF (nb != 0) THEN
moyenne := somme/nb;
moyenne := TRUNC (moyenne,1);
UPDATE PRATICIEN SET PRA_COEFCONF=moyenne WHERE PRA_NUM=pra_id;
END IF;
END;

Here are 2 limits that triggers are usually have:
when invoked for the all affected records, you don't know what exactly have changed
when invoked for individual records (FOR EACH ROW), you are limited with access to the modified table
To address that limitation, starting from Oracle 11g, we can use compound trigger:
CREATE OR REPLACE TRIGGER <trigger-name>
FOR <trigger-action> ON <table-name>
COMPOUND TRIGGER
-- Global declaration.
g_global_variable VARCHAR2(10);
-- block 1
BEFORE STATEMENT IS
BEGIN
NULL; -- Do something here.
END BEFORE STATEMENT;
-- block 2
BEFORE EACH ROW IS
BEGIN
NULL; -- Do something here.
END BEFORE EACH ROW;
-- block 3
AFTER EACH ROW IS
BEGIN
NULL; -- Do something here.
END AFTER EACH ROW;
-- block 4
AFTER STATEMENT IS
BEGIN
NULL; -- Do something here.
END AFTER STATEMENT;
END <trigger-name>;
And looks like this is what you need. In block 1, initialize your variables, in block 2 or 3 collect changes from individual rows, then in block 4 use that information to create the rest of the business logic.
If we are limited by 10g, then we can emulate compound trigger using package variables.
This solution is limited, because package variables are global for the session. If withing a session you have 2 similar operations, their results would be merged.
Here is the solution
You will have 3 separate triggers, that would represent block 1, (2 or 3), and 4 from the trigger above.
You will have a package with variable g_global_variable (from above)
3 actions:
1. in trigger for block 1 initiate g_global_variable
2. in trigger for block 2 or 3, populate it with actual values
3. in trigger for block 4, create your logic
Ofcourse, g_global_variable could be not alone, it could be record or collection.

Related

how to pass a whole row as record to a function in oracle triggers

I create a oracle package with function where in the input is a whole row and the output is varray. i want to use this package function to apply on all the rows of a table using trigger as soon as new record is inserted. I tried like this but the output is bad bind variable
create or replace trigger custdata_caferrors
before insert or update on customer_data
referencing new as n old as o
for each row
declare
v_remarks varchar(500) :=' ';
t_remarks caf_errors :=caf_errors();
rec customer_data%rowtype;
begin
-- rec = :n;
t_remarks := CUSTDATA_VERIFY.VERIFY_TERMSTATUS(:n);
for x in 1..t_remarks.count
loop
v_remarks :=v_remarks||' , '||t_remarks(x);
end loop;
:n.record_status1 :=v_remarks;
end;
/
Unfortunately there is no way to reference a whole new row in a trigger. I agree it would be neat if this were possible, but alas.
So you will have to explicitly populate your rec variable with the column values you require and pass that to your function, like this
create or replace trigger custdata_caferrors
before insert or update on customer_data
referencing new as n old as o
for each row
declare
v_remarks varchar(500) :=' ';
t_remarks caf_errors :=caf_errors();
rec customer_data%rowtype;
begin
rec.id := :n.id;
rec.col1 := :n.col1;
-- etc
t_remarks := CUSTDATA_VERIFY.VERIFY_TERMSTATUS(rec);
for x in 1..t_remarks.count
loop
v_remarks :=v_remarks||' , '||t_remarks(x);
end loop;
:n.record_status1 :=v_remarks;
end;
/
Do you really need to pass the whole row to CUSTDATA_VERIFY.VERIFY_TERMSTATUS()? If not you might get away with populating just the columns the function actually uses (although that does contravene the Law of Demeter).

Statement level trigger to enforce a constraint

I am trying to implement a statement level trigger to enforce the following "An applicant cannot apply for more than two positions in one day".
I am able to enforce it using a row level trigger (as shown below) but I have no clue how to do so using a statement level trigger when I can't use :NEW or :OLD.
I know there are alternatives to using a trigger but I am revising for my exam that would have a similar question so I would appreciate any help.
CREATE TABLE APPLIES(
anumber NUMBER(6) NOT NULL, /* applicant number */
pnumber NUMBER(8) NOT NULL, /* position number */
appDate DATE NOT NULL, /* application date*/
CONSTRAINT APPLIES_pkey PRIMARY KEY(anumber, pnumber)
);
CREATE OR REPLACE TRIGGER app_trigger
BEFORE INSERT ON APPLIES
FOR EACH ROW
DECLARE
counter NUMBER;
BEGIN
SELECT COUNT(*) INTO counter
FROM APPLIES
WHERE anumber = :NEW.anumber
AND to_char(appDate, 'DD-MON-YYYY') = to_char(:NEW.appDate, 'DD-MON-YYYY');
IF counter = 2 THEN
RAISE_APPLICATION_ERROR(-20001, 'error msg');
END IF;
END;
You're correct that you don't have :OLD and :NEW values - so you need to check the entire table to see if the condition (let's not call it a "constraint", as that term has specific meaning in the sense of a relational database) has been violated:
CREATE OR REPLACE TRIGGER APPLIES_AIU
AFTER INSERT OR UPDATE ON APPLIES
BEGIN
FOR aRow IN (SELECT ANUMBER,
TRUNC(APPDATE) AS APPDATE,
COUNT(*) AS APPLICATION_COUNT
FROM APPLIES
GROUP BY ANUMBER, TRUNC(APPDATE)
HAVING COUNT(*) > 2)
LOOP
-- If we get to here it means we have at least one user who has applied
-- for more than two jobs in a single day.
RAISE_APPLICATION_ERROR(-20002, 'Applicant ' || aRow.ANUMBER ||
' applied for ' || aRow.APPLICATION_COUNT ||
' jobs on ' ||
TO_CHAR(aRow.APPDATE, 'DD-MON-YYYY'));
END LOOP;
END APPLIES_AIU;
It's a good idea to add an index to support this query so it will run efficiently:
CREATE INDEX APPLIES_BIU_INDEX
ON APPLIES(ANUMBER, TRUNC(APPDATE));
dbfiddle here
Best of luck.
Your rule involves more than one row at the same time. So you cannot use a FOR ROW LEVEL trigger: querying on APPLIES as you propose would hurl ORA-04091: table is mutating exception.
So, AFTER statement it is.
CREATE OR REPLACE TRIGGER app_trigger
AFTER INSERT OR UPDATE ON APPLIES
DECLARE
cursor c_cnt is
SELECT 1 INTO counter
FROM APPLIES
group by anumber, trunc(appDate) having count(*) > 2;
dummy number;
BEGIN
open c_cnt;
fetch c_cnt in dummy;
if c_cnt%found then
close c_cnt;
RAISE_APPLICATION_ERROR(-20001, 'error msg');
end if;
close c_cnt;
END;
Obviously, querying the whole table will be inefficient at scale. (One of the reasons why triggers are not recommended for this sort of thing). So this is a situation in which we might want to use a compound trigger (assuming we're on 11g or later).

How can I apply condition to Insert data in Oracle Forms 6i

I have table DOC_CUST_PRODUCT (DOC_CODE, CUST_CODE, P_CODE)
I want to restrict insertion and show message when DOC_CODE has more than 5 different CUST_CODE
SELECT COUNT(DISTINCT CUST_CODE)
INTO Y
FROM BP_DOC_CUST_PRODUCT
WHERE DOC_CODE = :DOC_CODE;
IF NVL(Y,0) > 4 THEN
MESSAGE('Sorry, Can Not Entry More Than 5 Chemist...');
MESSAGE('Sorry, Can Not Entry More Than 5 Chemist...');
but it doesn't work.
Where did you put that code? Should be WHEN-VALIDATE-ITEM trigger on DOC_CODE item.
If there are two (or more) items named DOC_CODE, Forms doesn't know which one you're referencing - I suggest you to always specify block name with the item name.
Code you posted isn't complete - variable declaration is missing, IF doesn't have an END IF. I don't know whether you really didn't do that, or you just didn't post everything you wrote (by the way, how are we supposed to know that?).
COUNT function can't return NULL as a result, so applying NVL to the variable Y is superfluous.
The following code should be OK (if you fix what's missing - a block name).
-- WHEN-VALIDATE-ITEM trigger on :BLOCK_NAME.DOC_CODE item
declare
l_count number;
begin
select count(distinct b.cust_code)
into l_count
from bp_doc_cust_product b
where b.doc_code = :block_name.doc_code;
if l_count = 5 then
message('Sorry, ...');
message('Sorry, ...');
end if;
end;
You could achieve this by adding a post-query trigger to your block. If the count returns more than 5 elements, just change the ENABLE property of your item to disable it otherwise enable it.
To display your message before inserting a new record, you can also use an 'pre-insert' trigger on your block and check the result of the count, then display your message :
IF EL_COUNT > 5 THEN
Message('Sorry, Can Not Entry More Than 5 Chemist...');
RAISE Form_trigger_Failure;
END IF;

Oracle Trigger not read

I have issue with the following trigger.
This is my procedure that counts visits to a page:
create or replace procedure VisitSport(idN in number)
is
v1 number := idN;
vc number;
begin
select s.VISITAS into vc from Sports s where s.IDSPORTS=v1;
update Sports set VISITAS = vc +1 where IDSPORTS=v1;
end;
This is taking the score
create or replace PROCEDURE RankingSports(vnumber in number, vid in number)
is
v1 number := vnumber;
v2 number := vid;
v3 number;
begin
select CNT into v3 from SPORTS where IDSPORTS = v2;
update SPORTS set CNT= v3+v1 where IDSPORTS = v2;
end;
This is my trigger that makes the operation to calculate a Ranking
create or replace trigger trg_RankingSp
after insert or update on Sports
for each row
begin
update SPORTS set RANKING = visitas/cuenta;
end;
This is the error I get when trigger is executed.
A trigger (or a user defined plsql function that is referenced in
this statement) attempted to look at (or modify) a table that was
in the middle of being modified by the statement which fired it.
*Action: Rewrite the trigger (or function) so it does not read that table.
My guess is that you want to calculate the ranking value only for the row(s) being inserted or updated. Assuming that is correct, you would just update the :new pseudorecord rather than running an update.
create or replace trigger trg_RankingSp
after insert or update on Sports
for each row
begin
:new.RANKING := :new.visitas/:new.cuenta;
end;
Now, if it is possible for cuenta to have a value of 0, you'd want to handle that to prevent a division by 0 error.
From a data model standpoint, it seldom makes sense to store a computed value like this. It would generally make more sense to do the calculation in a view or, potentially, to create a virtual column that is automatically computed.

PL/SQL trigger to prevent duplicates

I have a table called TBLAPPLICATION which holds data specifying an individual's ID number and a JobID of the job they have applied for. Each ID number can have an unlimited number of applications, providing the JobID is different every time, thus having no duplicate applications.
create or replace
TRIGGER trg_duplicateapplication BEFORE INSERT ON tblapplication FOR EACH ROW
BEGIN
IF :NEW.studentrecordnumber_fk_nn = :OLD.studentrecordnumber_fk_nn THEN
IF :NEW.jobid_fk_nn = :OLD.jobid_fk_nn
THEN RAISE_APPLICATION_ERROR( -20003, 'Error: duplicate application. You have already applied for this position.');
END IF;
END IF;
END;
So the above code doesn't work, and I wish it would. Could anyone please highlight my mistake? :)
As it stands, your trigger is comparing the inserted values (:NEW.studentrecordnumber_fk_nn etc) with a non-existent :OLD (:OLD has no meaning to an INSERT trigger—it's fields are always null).
That aside, this should almost certainly be accomplished by DRI instead of a trigger at all— how about a unique index on (studentrecordnumber_fk_nn, jobid_fk_nn)?
You can use the MERGE statement in order to verify each couple (id,application) before inserting in the table (to check whether it is already in the table).
http://docs.oracle.com/cd/E11882_01/server.112/e26088/statements_9016.htm#SQLRF01606
Regards,
Dariyoosh
I am not sure that in your table TBLAPPLICATION which identifier is unique (maybe JobID?) and which you want you not to be duplicated (maybe studentrecordnumber_fk_nn?). But I created a script to prevent duplication on studentrecordnumber_fk_nn.
And in my example “alphabet” I wrote a totally similar script to prevent the duplication: you cannot insert a letter which was inserted into the table earlier.
I hope it will help.
z
CREATE OR REPLACE TRIGGER trg_duplicateapplication
BEFORE INSERT
ON tblapplication
FOR EACH ROW
DECLARE
counter integer;
BEGIN
SELECT * INTO counter FROM
(SELECT COUNT(rownum) FROM tblapplication a
WHERE a.studentrecordnumber_fk_nn = :new.studentrecordnumber_fk_nn);
IF counter = 1 THEN
RAISE_APPLICATION_ERROR( -20003,
'Error: duplicate application. You have already applied for this position.');
END IF;
END;
/
––The Alphabet
CREATE TABLE alphabet
(letter VARCHAR2(2));
INSERT INTO alphabet VALUES ('A');
INSERT INTO alphabet VALUES ('B');
INSERT INTO alphabet VALUES ('C');
INSERT INTO alphabet VALUES ('D');
CREATE OR REPLACE TRIGGER insertvalue
BEFORE INSERT
ON alphabet
FOR EACH ROW
DECLARE counter INTEGER;
BEGIN
SELECT * INTO counter FROM
(SELECT COUNT(rownum) FROM alphabet a WHERE a.letter = :new.letter);
IF counter = 1 THEN
RAISE_APPLICATION_ERROR(-20012,'Duplicated value');
END IF;
END;
/

Resources