PL/SQL Trigger Variable Problems - oracle

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.

Related

counting number of cars that each owner has

I'm trying to learn plsql and got stuck in understanding some basic stuff. Here is a challenge that I'm trying to solve.
I have two tables. One holds information about owners and the other is information about cars. I want to to write an anonymous block that joins these two tables and with a for loop based on amount of cars that is registered to each owner prints how many cars each person own. furthermore I want an if statement which distinguishes between 1 Car (singular) and 2, 3 Cars (plural).
the tables are these:
CREATE TABLE owners(
id_nr VARCHAR2(13) PRIMARY KEY,
f_name VARCHAR2(20),
s_name VARCHAR2(20));
CREATE TABLE cars(
reg_nr VARCHAR2(6) PRIMARY KEY,
id_nr REFERENCES owners(pnr),
model VARCHAR2(20),
year NUMBER(4),
date DATE);
The result may look like something like this:
19380321-7799, Hans, Anderson, Owns: 1 car
19490321-7899, Mike, Erikson, Owns: 2 cars
.
.
.
etc.
I tried many different ways but each time i get some errors.
I would appreciate any help and hints that helps me understand it.
Thanks!
Well, here's one way to do it. If you want to practice loops, you could add a second loop inside to loop over the cars table and print all the cars each owner has.
declare
v_suffix varchar2(1);
begin
for o in (select owners.id_nr, f_name, s_name,
(select count(1) from cars where cars.id_nr = owners.id_nr) as num_cars
from owners)
loop
if o.num_cars = 1
then v_suffix = null -- singular
else v_suffix = 's' -- plural
end if;
dbms_output.put_line(o.id_nr || ', ' || o.f_name || ', ' || o.s_name
|| ' Owns: ' || o.num_cars || ' car' || v_suffix);
end loop;
end;
/

How to fix 'Error(9,7): PL/SQL: ORA-00947: not enough values' in a function?

I'm trying to get a total commission for each Sale staff and store it to a function to put it in a procedure, when I put a working select statement (three tables involved), I got Error(9,20): PL/SQL: ORA-00947: not enough values. I think because datatype only returns number in this function, but the table contains other varchar datatypes that cause this problem.
I tried to remove some columns that are varchar2 datatype but the result is not correct.
Below is my fictional code:
create or replace FUNCTION get_total_commission return number
IS
v_total_commission;
--
begin
select
sale_id, sale_acct,sale_name, sum(commission) as total_commission
into v_total from invoice_tbl invoice join commission_tbl commission
on invoice.id = commission.id join sale_tbl sale on sale.id = commssion.id
where invoice.refnr is null;
return to_char(v_total, 'FM99999.00');
EXCEPTION
WHEN OTHERS THEN
dbms_output.put_line('err: ' ||SQLERRM);
end get_total_commission;
It will be a function that will show the total amount of commission that earns by each Sale staff.
You need to use local variable to which all your four columns in the SELECT list return to. And because of conversion to character type, need to return a string type value instead of numeric for the function.
SQL> SET SERVEROUTPUT ON;
SQL> CREATE OR REPLACE FUNCTION get_total_commission RETURN varchar2 IS
v_total_commission commission_tbl.commission%type;
v_sale_id sale_tbl.sale_id%type;
v_sale_acct sale_tbl.sale_acct%type;
v_sale_name sale_tbl.sale_name%type;
BEGIN
select sale_id, sale_acct, sale_name, sum(commission) as total_commission
into v_sale_id, v_sale_acct, v_sale_name, v_total
from invoice_tbl invoice
join commission_tbl commission
on invoice.id = commission.id
join sale_tbl sale
on sale.id = commssion.id
where invoice.refnr is null;
return to_char(v_total, 'FM99999.00');
EXCEPTION
WHEN OTHERS THEN
dbms_output.put_line('err: ' || SQLERRM);
END;
the total amount of commission that earns by each Sale staff
Sounds like returning one number shorn of identifying characteristics is not the solution you need. You need a result set. Personally, this seems better fitted to a view than a function but if you want to wrap the query in a function this is how to do it:
-- obviously correct these data types to fit your actual needs
create or replace type commission_t as object(
sale_id varchar2(30)
, acct_id varchar2(30)
, sale_name varchar2(48)
, total_commission_number
);
/
create or replace type commission_nt as table of commission_t;
/
create or replace FUNCTION get_total_commission return commission_nt
IS
return_value commission_nt;
begin
select commission_t(
sale_id, sale_acct,sale_name, sum(commission) )
bulk collect into return_type
from invoice_tbl invoice
join commission_tbl commission on invoice.id = commission.id
join sale_tbl sale on sale.id = commssion.id
where invoice.refnr is null
group by sale_id, sale_acct,sale_name
;
return return_value;
end get_total_commission;
And query it like this:
select * from table (get_total_commission);
There are various rough edges with this. For instance it won't work well if your result set is huge (which obviously depends, but say more than 5000-10000 rows).
If you really just want the total commission for a single sale then you need to restrict the query by SALE_ID - and pass it as a parameter:
create or replace FUNCTION get_total_commission
(p_sale_id in sale.id%type)
return number
IS
v_total_commission number;
--
begin
select
sum(commission) as total_commission
into v_total_commission
from invoice_tbl invoice
join commission_tbl commission on invoice.id = commission.id
join sale_tbl sale on sale.id = commssion.id
where sale.id = p_sale_id
and invoice.refnr is null;
return v_total_commission ;
end get_total_commission;

Any efficient solution for that

declare
cursor cur1 is select * from address where aid in
(select Min(aid) from address group by
country,state,city,street_name,locality,house_no);
cursor cur2 is select * from address;
cur1_aid address.aid%type;
cur1_country address.country%type;
cur1_city address.city%type;
cur1_state address.state%type;
cur1_streetAddress address.street_name%type;
cur1_locality address.locality%type;
cur1_houseNo address.house_no%type;
cur2_aid address.aid%type;
cur2_country address.country%type;
cur2_city address.city%type;
cur2_state address.state%type;
cur2_streetAddress address.street_name%type;
cur2_locality address.locality%type;
cur2_houseNo address.house_no%type;
begin
open cur1;
loop
fetch cur1 into cur1_aid,cur1_country,cur1_state,cur1_city,cur1_streetAddress,cur1_locality,cur1_houseNo;
exit when cur1%NOTFOUND;
open cur2;
loop
fetch cur2 into cur2_aid,cur2_country,cur2_state,cur2_city,cur2_streetAddress,cur2_locality,cur2_houseNo;
exit when cur2%NOTFOUND;
if(cur1_country=cur2_country) and (cur1_state=cur2_state) and (cur1_city=cur2_city) and (cur1_streetAddress=cur2_streetAddress) and (cur1_locality=cur2_locality) and (cur1_houseNo=cur2_houseNo) then
if (cur1_aid!=cur2_aid) then
update employee_add set aid=cur1_aid where aid=cur2_aid;
delete address where aid=cur2_aid;
end if;
end if;
end loop;
close cur2;
end loop;
close cur1;
DELETE FROM employee_add a
WHERE ROWID > (SELECT MIN(ROWID) FROM employee_add b
WHERE b.eid=a.eid and b.aid=a.aid
);
end;
/
I have three table Employee(eid,ename) ,Address(aid,country,state,city,streetaddress,locality,houseNo) and a relationship table (M2M) MANY TO MANY TABLE employee_add(eid,aid),
I want to remove duplicates from address table and employee_add table without data loss
Assuming this is a one time de-duplication you could:
Create a new temporary set of eid <-> aid relationships based on the current address attached to an employee and always pick the min address record with matching data (this is what you are doing above)
Delete existing eid <-> aid relationships
Insert new relationships from step 1, drop step 1 data
Delete addresses that no longer have any employee attached
Something like this (untested as you did not provide any DDL or DML to create a working example from):
-- Step 1
CREATE TABLE employee_add_new AS
SELECT ea.eid,
(SELECT MIN(a2.aid)
FROM address a2
WHERE a2.country = a.country
AND a2.state = a.state
AND a2.city = a.city
AND a2.street_name = a.street_name
AND a2.locality = a.locality
AND a2.house_no = a.house_no) AS aid
FROM employee_add ea
INNER JOIN address a
ON a.aid = ea.aid;
-- Step 2
TRUNCATE TABLE employee_add;
-- Step 3
INSERT INTO employee_add
(eid,
aid)
SELECT eid,
aid
FROM employee_add_new;
DROP TABLE employee_add_new;
-- Step 4
DELETE FROM address a
WHERE NOT EXISTS (SELECT NULL
FROM employee_add ea
WHERE ea.aid = a.aid);
You could also change step 2 and 3 to drop the existing employee_add table and rename employee_add_new to employee_add, but I have no idea what your table structure looks like (columns, FKs, indexes, etc).

Oracle trigger return after update

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. 

Oracle: Product Stock vs Quantity Trigger

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

Resources