I am trying to create a trigger named invoices_after_update_payment for the Invoices table
that displays the vendor name, invoice number, and payment total in the output
window whenever the payment total is increased.
This is my first time working with triggers and all I am getting is errors
create or replace trigger invoices_after_update_payment
after update
on invoices
for each row
when (new.payment_total > old.payment_total)
declare
vendor_name_var vendors%rowtype%;
Begin
Select v.vendor_name, i.invoice_number, i.payment_total
into vendor_name_var, :new.invoice_number, :new.payment_total
from Vendors v
inner join Invoices i
on v.vendor_id = i.vendor_id
where i.vendor_id = :new.vendor_id
dbms_output.put_line(vendor_name_var || :new.invoice_number || :new.payment_total);
end;
/
Put semi colon to terminate this line like
where i.vendor_id = :new.vendor_id;
and compile to see whether you are getting any errors.
Related
SELECT CIF_ID,
SUM (IN_VERIFIED_DEBT + IN_FAC_WITH_OTHER + IN_FAC_WITH_BANK)
from LOS_CIF_INDV
WHERE STATUS= 'ACTIVE'
GROUP By CIF_ID;
I want to update the total column again after the user manipulates the client as update, insert but it gives an error
ORA-04098: trigger 'RLOS138.UPDATE_IN_TOTAL_COMMIT' is invalid and failed re-validation
CREATE OR REPLACE TRIGGER UPDATE_IN_TOTAL_COMMIT
AFTER UPDATE ON
LOS_CIF_INDV
FOR EACH ROW
DECLARE
inactive_id number;
BEGIN
inactive_id:=
:new.IN_VERIFIED_DEBT + :new.IN_FAC_WITH_OTHER + :new.IN_FAC_WITH_BANK;
UPDAte LOS_CIF_INDV
SET IN_TOTAL_COMMIT = inactive_id
WHERE CIF_ID = :NEW.CIF_ID;
END ;
/
I have tried this again
CREATE OR REPLACE TRIGGER RLOS138.UPDATE_IN_TOTAL_COMMIT
AFTER UPDATE ON RLOS138.LOS_CIF_INDV
FOR EACH ROW
DECLARE
inactive_id number;
BEGIN
SELECT SUM (IN_VERIFIED_DEBT+IN_FAC_WITH_OTHER+IN_FAC_WITH_BANK)
into inactive_id
from LOS_CIF_INDV
WHERE STATUS= 'ACTIVE'
and CIF_ID=:NEw.CIF_ID;
update LOS_CIF_INDV
set IN_TOTAL_COMMIT = inactive_id
where CIF_ID = :NEW.CIF_ID;
END ;
/
yes [CIF_ID] is primary key
In which case this trigger has the logic you need:
CREATE OR REPLACE TRIGGER RLOS138.UPDATE_IN_TOTAL_COMMIT
BEFORE UPDATE ON RLOS138.LOS_CIF_INDV
FOR EACH ROW
BEGIN
if :new.status = 'ACTIVE'
then
:new.IN_TOTAL_COMMIT := :new.IN_VERIFIED_DEBT + :new.IN_FAC_WITH_OTHER + :new.IN_FAC_WITH_BANK;
end if;
END ;
/
I have included the check on status because you used it in your aggregation queries, even though you omitted from the first version of the trigger. I haven't included an ELSE branch, but you may wish to add one. Also, I have assumed that the three columns in the addition are guaranteed to be not null; if that's not the case you'll need to handle that.
I have put a working demo on db<>fiddle. This includes a version of the trigger which fires on inserts as well as updates, and handles null values too....
CREATE OR REPLACE TRIGGER UPDATE_IN_TOTAL_COMMIT
-- handle INSERT as well as UPDATE
BEFORE INSERT OR UPDATE ON LOS_CIF_INDV
FOR EACH ROW
BEGIN
if :new.status = 'ACTIVE'
then
-- handle any of these columns being null
:new.IN_TOTAL_COMMIT := nvl(:new.IN_VERIFIED_DEBT,0)
+ nvl(:new.IN_FAC_WITH_OTHER,0)
+ nvl(:new.IN_FAC_WITH_BANK,0);
end if;
END ;
/
Why not after you could explain it to me
Because Oracle have written triggers that way: the AFTER EACH ROW trigger uses the finalised version of the record, the state which will be written to the database. Consequently, if we want to change any values we need to use a BEFORE EACH ROW trigger. Oracle enforces this with the error you got, ORA-04084: cannot change NEW values for this trigger type.
Just a reminder: ORA-04098 is telling you there are compilation errors in your trigger code. If you're not using an IDE which tells you what these errors are you can find them with this query:
select * from all_errors
where owner = 'RLOS138'
and name = 'UPDATE_IN_TOTAL_COMMIT' ;
(Not sure if you're connecting as RLOS138 - if you are, query USER_ERRORS instead.)
If I understood correctly, You want to update all the records having CIF_ID as an updated record with the same value in the IN_TOTAL_COMMIT column.
This is not a good idea. If you have some derived column then you should use the views instead of updating its value for every insert/update using the trigger.
If you really want to update the column then you must use the combination of Row level trigger, Statement trigger, and package variables. (Search for mutating table error in the SO)
But according to me, the best solution is to use the view, something like follows:
CREATE OR REPLACE VIEW LOS_CIF_INDV_VW AS
SELECT L.*,
COALESCE(
SUM(
CASE
WHEN STATUS = 'ACTIVE' THEN
IN_VERIFIED_DEBT + IN_FAC_WITH_OTHER + IN_FAC_WITH_BANK
END
) OVER(
PARTITION BY L.CIF_ID
),
0
) AS IN_TOTAL_COMMIT
FROM LOS_CIF_INDV L;
I am relatively new to PL/SQL and i am trying to create a trigger that will alert me after an UPDATE on a table Review. When it is updated I want to ge the username(User table), score(Review Table), and product name (Product Table) and print them out:
This is what I have so far:
three tables:
Review: score, userid,pid, rid
Users: userid,uname
Product: pid,pname
So Review can reference the other tables with forigen keys.
create or replace trigger userNameTrigger
after insert on review
for each row
declare
x varchar(256);
y varchar(256);
z varchar(256);
begin
select uname into x , pname into y , score into z
from review r , product p , users u
where r.pid = p.pid and r.userid = u.userid and r.rid =new.rid;
dbms_output.put_line('user: '|| X||'entered a new review for Product: '|| Y || 'with a review score of: '|| Z);
end;
The problem I am having is I cannot seem to figure out how to store the selected fields into the variables and output it correctly.
DDL:
Create Table Review
(
score varchar2(100)
, userid varchar2(100)
, pid varchar2(100)
, rid varchar2(100)
);
Create Table Users
(
userid varchar2(100)
, uname varchar2(100)
);
Create Table Product
(
pid varchar2(100)
, pname varchar2(100)
);
The first problem I can see is that you're missing a colon when you refer to new.rid. The second is that you're accessing the review table inside a row-level trigger on that same table, which will give you a mutating table error at some point; but you don't need to as all the data from the inserted row is in the new pseudorow.
create or replace trigger userNameTrigger
after insert on review
for each row
declare
l_uname users.uname%type;
l_pname product.pname%type;
begin
select u.uname into l_uname
from users u
where u.userid = :new.userid;
select p.pname
into l_pname
from product
where p.pid = :new.pid;
dbms_output.put_line('user '|| l_uname
|| ' entered a new review for product ' || l_pname
|| ' with a review score of '|| :new.score);
end;
The bigger problem is that the only person who could see the message is the user inserting tow row, which seems a bit pointless; and they would have to have output enabled in their session to see it.
If you're trying to log that so someone else can see it then store it in a table or write it to a file. As the review table can be queried anyway it seems a bit redundant though.
Having all your table columns as strings is also not good - don't store numeric values (e.g. scores, and probably the ID fields) or dates as strings, use the correct data types. It will save you a lot of pain later. You also don't seem to have any referential integrity (primary/foreign key) constraints - so you can review a product that doesn't exist, for instance, which will cause a no-data-found exception in the trigger.
It makes really no sense to use a trigger to notify themselves about changed rows. If you insert new rows into the table, then you have all info about them. Why not something like the block below instead a trigger:
create table reviews as select 0 as rid, 0 as userid, 0 as score, 0 as pid from dual where 1=0;
create table users as select 101 as userid, cast('nobody' as varchar2(100)) as uname from dual;
create table products as select 1001 as pid, cast('prod 1001' as varchar2(100)) as pname from dual;
<<my>>declare newreview reviews%rowtype; uname users.uname%type; pname products.pname%type; begin
insert into reviews values(1,101,10,1001) returning rid,userid,score,pid into newreview;
select uname, pname into my.uname, my.pname
from users u natural join products p
where u.userid = newreview.userid and p.pid = newreview.pid
;
dbms_output.put_line('user: '||my.uname||' entered a new review for Product: '||my.pname||' with a review score of: '||newreview.score);
end;
/
output: user: nobody entered a new review for Product: prod 1001 with a review score of: 10
In order to inform another session about an event you should use dbms_alert (transactional) or dbms_pipe (non transactional) packages. An example of dbms_alert:
create or replace trigger new_review_trig after insert on reviews for each row
begin
dbms_alert.signal('new_review_alert', 'signal on last rid='||:new.rid);
end;
/
Run the following block in another session (new window, worksheet, sqlplus or whatever else). It will be blocked until the registered signal is arrived:
<<observer>>declare message varchar2(400); status integer; uname users.uname%type; pname products.pname%type; score reviews.score%type;
begin
dbms_alert.register('new_review_alert');
dbms_alert.waitone('new_review_alert', observer.message, observer.status);
if status != 0 then raise_application_error(-20001, 'observer: wait on new_review_alert error'); end if;
select uname, pname, score into observer.uname, observer.pname, observer.score
from reviews join users using(userid) join products using (pid)
where rid = regexp_substr(observer.message, '\w+\s?rid=(\d+)', 1,1,null,1)
;
dbms_output.put_line('observer: new_review_alert for user='||observer.uname||',product='||observer.pname||': score='||observer.score);
end;
/
Now in your session:
insert into reviews values(2, 101,7,1001);
commit; --no alerting before commit
The another (observer) session will be finished with the output:
observer: new_review_alert for user=nobody,product=prod 1001: score=7
P.S. There was no RID in the Table REVIEW, so i'll just assume it was supposed to be PID.
create or replace trigger userNameTrigger
after insert on review
for each row
declare
x varchar2(256);
y varchar2(256);
z varchar2(256);
BEGIN
select uname
, pname
, score
INTO x
, y
, z
from review r
, product p
, users u
where r.pid = p.pid
and r.userid = u.userid
and r.PID = :new.pid;
dbms_output.put_line('user: '|| X ||'entered a new review for Product: '|| Y || 'with a review score of: '|| Z);
end userNameTrigger;
You just made a mistake on the INTO statement, you can just clump them together in one INTO.
New to working with PL/SQL and trying to create a statement level trigger that will change the 'Reorder' value to 'Yes' when the product quantity (p_qoh) is either less than 10 or less than two times the product minimum (p_min). And if that's not the case, then to change the 'Reorder' value to 'No'. My problem is that when I perform an update for a specific product, it changes the reorder value of all rows instead of the one I'm specifying. Can't seem to figure out where I'm going wrong, think I've been staring at it too long, any help is greatly appreciated.
CREATE OR REPLACE TRIGGER TRG_AlterProd
AFTER INSERT OR UPDATE OF p_qoh, p_min ON product
DECLARE
v_p_min product.p_min%type;
v_p_qoh product.p_qoh%type;
CURSOR v_cursor IS SELECT p_min, p_qoh FROM product;
BEGIN
OPEN v_cursor;
LOOP
FETCH v_cursor INTO v_p_min, v_p_qoh;
EXIT WHEN v_cursor%NOTFOUND;
IF v_p_qoh < (v_p_min * 2) OR v_p_qoh < 10 THEN
UPDATE product SET p_reorder = 'Yes';
ELSE
UPDATE product SET p_reorder = 'No';
END IF;
END LOOP;
END;
/
The update command :
UPDATE product SET p_reorder = 'Yes';
updates all of your rows because you are not specifying a WHERE clause.
What you can do is to retrieve the product's id (product_id) using your cursor and save it so that you would use it this way:
UPDATE product SET p_reorder = 'Yes' WHERE id = product_id;
Whoaa, this is not how you do triggers.
1 - Read the Oracle Trigger Documentation
2 - (almost) Never do a commit in a trigger. That is the domain of the calling application.
3 - There is no need to select anything related to product. You already have the product record at hand with the :new and :old pseudo records. Just update the column value in :new as needed. Example below (not checked for syntax errors, etc.);
CREATE OR REPLACE TRIGGER TRG_AlterProd
BEFORE INSERT OR UPDATE OF p_qoh, p_min ON product
FOR EACH ROW
BEGIN
IF :new.p_qoh < (:new.p_min * 2) OR :new.p_qoh < 10 THEN
:new.p_reorder = 'Yes';
ELSE
:new p_reorder = 'No';
END IF;
END;
#StevieP, If you need to commit inside a trigger, you may want to consider doing it as Autonomous Transaction.
Also, sorry if my understanding of your problem statement is wrong, but your it sounded to me like a row level trigger - are you only updating the current row or are you scanning the entire table to change status on several rows? If it's on current row, #OldProgrammer's solution seems right.
And I am just curious, if you do an UPDATE statement inside the trigger on the same table, wouldn't it generate (recursive) trigger(s)? I haven't done statement triggers like this, so sorry if this is not the expected trigger behavior.
To me a statement trigger would make more sense, if the trigger was on say, sales table, when a product is sold (inserted into sales table), it will trigger the corresponding product id records to be updated (to REORDER) in Product table. That will prevent recursion danger also.
Hopefully this is the last of many questions about triggers! Still working with the same database where the Order_line entity is a link entity between Order and Products. With this trigger I just want to check if the current order quantity is greater than the stock in Products. At the moment I would be doing this by using two variables, Ordered(quantity) and Total(Stock) and comparing them, but this isn't working.
If the quantity is greater than the stock the record being inserted must be deleted and an error is raised.
CREATE OR REPLACE TRIGGER Checks_Order
BEFORE INSERT ON order_line
FOR EACH ROW
DECLARE
ordered int;
total INT;
BEGIN
SELECT ol.quantity INTO ordered FROM order_line ol WHERE
ol.product_no = :new.product_no;
if(ordered>0) then
SELECT p.stock INTO total FROM
products p WHERE p.product_no = :new.product_no;
IF (ordered < total) then
DELETE FROM order_line ol where ol.order_no = :new.order_no;
RAISE_APPLICATION_ERROR(-20103, 'Not enough stock!');
END IF;
END IF;
END;
.
run
Help, please?
The trigger will not work because you cannot select or even delete from the table that the trigger belongs to.
But you don't need to actually, the value that is ordered can be obtained through :new.quantity.
And if you raise an error, the INSERT will not happen, no need to DELETE the row.
So - assuming I understood your intention correctly - the following should do what you want:
CREATE OR REPLACE TRIGGER Checks_Order
BEFORE INSERT ON order_line
FOR EACH ROW
DECLARE
total INT;
BEGIN
if (:new.quantity > 0) then
SELECT p.stock
INTO total
FROM products p
WHERE p.product_no = :new.product_no;
IF (:new.quantity > total) then
RAISE_APPLICATION_ERROR(-20103, 'Not enough stock!');
END IF;
END IF;
END;
/
Btw: I guess you want :new.quantity > total not < total
CREATE OR REPLACE TRIGGER "DISC_CLIENT"
BEFORE INSERT ON "PURCHASE"
FOR EACH ROW
DECLARE
checkclient PURCHASE.CLIENTNO%TYPE;
BEGIN
SELECT Clientno INTO checkclient
FROM PURCHASE
GROUP BY ClientNo
HAVING SUM(Amount)=(SELECT MAX(SUM(Amount)) FROM PURCHASE GROUP BY Clientno);
IF :new.ClientNo = checkclient
new.Amount := (:old.Amount * 0.90);
END IF;
END;
/
Seem to be having a problem with this trigger. I know there I cant use the WHEN() clause for subqueries so im hoping this would work but it doesnt! Ideas anyone? :/
Basically im trying to get this trigger to apply a discount to the amount value before inserting if the client matches the top client! : )
There's a non-pretty but easy way round this, create a view and update that. You can then explicitly state all the columns in your trigger and put them in the table. You'd also be much better off creating a 1 row 2 column table, max_amount and then inserting the maximum amount and clientno into that each time. You should also really have a discounted amount column in the purchase table, as you ought to know who you've given discounts to. The amount charged is then amount - discount. This get's around both the mutating table and being unable to update :new.amount as well as making your queries much, much faster. As it stands you don't actually apply a discount if the current transaction is the highest, only if the client has placed the previous highest, so I've written it like that.
create or replace view purchase_view as
select *
from purchase;
CREATE OR REPLACE TRIGGER TR_PURCHASE_INSERT
BEFORE INSERT ON PURCHASE_VIEW
FOR EACH ROW
DECLARE
checkclient max_amount.clientno%type;
checkamount max_amount.amount%type;
discount purchase.discount%type;
BEGIN
SELECT clientno, amount
INTO checkclient, checkamount
FROM max_amount;
IF :new.clientno = checkclient then
discount := 0.1 * :new.amount;
ELSIF :new.amount > checkamount then
update max_amount
set clientno = :new.clientno
, maxamount = :new.amount
;
END IF;
-- Don-t specify columns so it breaks if you change
-- the table and not the trigger
insert into purchase
values ( :new.clientno
, :new.amount
, discount
, :new.other_column );
END TR_PURCHASE_INSERT;
/
As I remember a trigger can't select from a table it's fired for.
Otherwise you'll get ORA-04091: table XXXX is mutating, trigger/function may not see it. Tom advises us not to put too much logic into triggers.
And if I understand your query, it should be like this:
SELECT Clientno INTO checkclient
FROM PURCHASE
GROUP BY ClientNo
HAVING SUM(Amount)=(select max (sum_amount) from (SELECT SUM(Amount) as sum_amount FROM PURCHASE GROUP BY Clientno));
This way it will return the client who spent the most money.
But I think it's better to do it this way:
select ClientNo
from (
select ClientNo, sum (Amount) as sum_amount
from PURCHASE
group by ClientNo)
order by sum_amount
where rownum