PL/SQL Update Trigger Updates All Rows - oracle

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.

Related

2 users running a stored procedure concurrently - what happens if DML statements in one execution affect tests/conditions in the parallel execution?

Let's say I have a PL/SQL stored procedure for creating sales orders as follows:
CREATE OR REPLACE PROCEDURE save_order
(
o_output_status OUT BINARY_INTEGER,
i_customer IN VARCHAR2,
i_product_code IN VARCHAR2,
i_quantity IN BINARY_INTEGER
)
IS
l_stock_available BINARY_INTEGER;
BEGIN
o_output_status := -1;
SELECT available
INTO l_stock_available
FROM stock
WHERE product = i_product_code
;
IF l_stock_available >= i_quantity THEN
INSERT INTO sales_orders (order_number, customer, product, quantity)
VALUES (order_seq.nextval, i_customer, i_product_code, i_quantity)
;
-- hello
UPDATE stock
SET available = available - i_quantity
WHERE product = i_product_code
;
o_output_status := 0;
END IF;
END save_order;
I think that's all pretty straightforward. But what I'd like to know is what happens when 2 users run this stored procedure concurrently. Let's say there's only 1 unit of some product left. If user1 runs the stored procedure first, attempting to create an order for 1 unit, l_stock_available gets a value of 1, the IF condition evaluates to true and the INSERT and UPDATE get executed.
Then user2 runs the stored procedure a moment later, also trying to create an order for 1 unit. Let's say the SELECT INTO for user2 gets executed by Oracle at the instant that user1's execution has reached the comment hello. At this point, user2 will also get a value of 1 for l_stock_available, the IF condition will evaluate to true, and the INSERT and UPDATE will be executed, bringing the stock level down to -1.
Do I understand correctly? Is this what will happen? How can I avoid this scenario, where 2 orders can be created for the last item in stock?
Yes, you understand correctly that the code as written has a race condition.
The simplest fix, assuming that performance requirements permit pessimistic locking, is to add a FOR UPDATE to the initial SELECT statement. That locks the particular row in the STOCKS table which would cause the second session to block until the first session's transaction either commits or rolls back. The second session would then see that the stock on hand had been decreased to 0.
If you want to implement DIY optimistic locking (using the similar mechanisms as e.g. JPA uses) add a VERSION column to your stock table with data type INT initialized with zero.
The initial selectof your procedure returns not only the available quantity but also the current VERSION:
select available, version
into l_available,l_version
from stock
where product_id = i_product_code;
If the table has the required quantity you first UPDATE it, but only if the VERSION has the same value as returned from the previous query.
update stock
set available = available - i_quantity,
version = version + 1
where product_id = 1 and
/* optimistick locking */
version = l_version;
Note that 1) the update will fail if the version does not match the value you selected.
If the update succeeds you increase the version to block others from performing an concurrent UPDATE.
The last step is to check if the update was succesful and if so you process the quantity you got.
rn := SQL%rowcount;
if rn = 1 then /* success */
insert into sales_orders (thread_id, product_id,quantity,create_ts)
values(i_thread_id, i_product_code,i_quantity,current_timestamp);
commit;
end if;
If the UPDATE changed zero rows (if failed due to changed VERSION) you should either retry of return a failure.

Can I make an update trigger using data from one table to update another?

I'm trying to get the suspension value in my studentstaff table to change from 'no' to 'yes' when the fine (a separate table) amount reaches >=10 for a specific person. I've also tried using IF but nothings seems to be working as I keep getting this error: ORA-04079: invalid trigger specification. "amount NUMBER(8);" is in the code as it was asking me to declare amount. I am using Oracle SQL. Thanks in advance.
CREATE TRIGGER new_suspension
AFTER UPDATE ON fine
FOR EACH ROW
amount NUMBER(8);
BEGIN
CASE
WHEN Amount >= 10 THEN
UPDATE studentstaff
SET suspensions = 'yes'
WHERE library_card_no = '419746';
END CASE;
END;
/
The best option for this type of trigger is to use the when condition on trigger as follows:
CREATE OR REPLACE TRIGGER new_suspension
AFTER UPDATE ON fine
FOR EACH ROW
WHEN (NEW.AMOUNY >= 10)
BEGIN
UPDATE studentstaff
SET suspensions = 'yes'
WHERE library_card_no = :new.LIBRARY_CARD_HOLDER;
END;
/
This trigger will be executed when the condition written in the WHEN clause is satisfied.
Without seeing your table definitions its hard, but here's a guess at what you might need
CREATE OR REPLACE
TRIGGER new_suspension
AFTER UPDATE ON fine
FOR EACH ROW
BEGIN
if :new.Amount >= 10 THEN -- ie, the incoming AMOUNT on the FINE table
UPDATE studentstaff
SET suspensions = 'yes'
WHERE library_card_no = :new.LIBRARY_CARD_HOLDER -- ie, the STUDENT being fined
end if;
END;
/
Hopefully that makes sense and is close to what you're aiming to achieve here. In this example, I'm assuming that your FINE table, contains the amount and the library card holder (ie, the student etc)

Update after calculate for each record ORACLE

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;

Oracle Trigger Conditional Rows

I'am just exploring Trigger in Oracle.
I have table like this
Note : WT_ID Id is FK of Water Summary but not have constraint(not directly connected)
I want to make trigger in Temp_tank table, if there are update in Table Temp_tank, it will sum all temp_tank volume with same WT_ID then updated it to Water_summary.Water_Use. Because of bussiness requirement, not all water_summary data will update. in this example only Home A will be affected
This is MyCode
CREATE OR REPLACE TRIGGER UPD_WaterUse
AFTER UPDATE ON Temp_tank
DECLARE
temp_wat number;
homeA_id= 1;
BEGIN
IF (WT_ID = homeA_id) THEN
SELECT SUM(ss.Volume) INTO temp_wat
from Temp_tank ss WHERE ss.Daytime = DAYTIME and ss.WT_ID =homeA_id;
-- functionUpdate(homeA_id,Daytime,temp_wat) ;
ELSE
NULL;
END IF;
END;
/
The question is, in line
IF (WT_ID = homeA_id) THEN
when i compiled, the line is ignored because WT_ID is not identifier.
is trigger cannot accept this style of code?

Trigger to calculate subtotal

I've been trying to implement this trigger for a while now and am making progress (I think!) but now I am getting a mutation error.
What I have here is three entities (that are relevant here), Customer_Order(total etc), Order_Line(quantity, subtotal etc) and Products(stock, price). Order_line is a link entity and so a product can be in many order_lines and a customer_order can have many order_lines, but an order_line can only appear once in an order and can only contain one product. The purpose of the trigger is to take the subtotal from order_line(or price from products I think actually) and the quantity from order_line, multiply them and update the new order_line's subtotal.
So I insert an order_line with my product foreign key, quantity of 3 and price of 4.00, the trigger multiplies the two to equal 12 and updates the subtotal. Now, I am thinking it's right to use price here instead of Order_line's subtotal in order to fix the mutation error (which occurs because I am asking the trigger to update the table which is being accessed by the triggering statement, right?), but how do I fix the quantity issue? Quantity won't always be the same value as stock, it has to be less than or equal to stock, so does anyone know how I can fix this to select from product and update order_line? Thanks.
CREATE OR REPLACE TRIGGER create_subtotal
BEFORE INSERT OR UPDATE ON Order_Line
for each row
DECLARE
currentSubTotal order_line.subtotal%type;
currentQuantity order_line.quantity%type;
BEGIN
select order_line.subtotal,order_line.quantity
into currentSubTotal,currentQuantity
from order_line
where product_no = :new.product_no;
IF (currentquantity>-1 ) then
update order_line set subtotal= currentSubTotal * currentQuantity where line_no=:new.line_no;
END IF;
END;
.
run
EDIT: I think I could use the :new syntax to use the quantity value from the triggering statement. I'll try this but I'd appreciate confirmation and help still, thanks.
It sounds like you want something like
CREATE OR REPLACE TRIGGER create_subtotal
BEFORE INSERT OR UPDATE ON order_line
FOR EACH ROW
DECLARE
l_price products.price%type;
BEGIN
SELECT price
INTO l_price
FROM products
WHERE product_no = :new.product_no;
IF( :new.quantity > -1 )
THEN
:new.subtotal := :new.quantity * l_price;
END IF;
END;
If this is something other than homework, however, it doesn't really make sense to pull the price from the PRODUCTS table in this trigger. Presumably, a product's price will change over time. But the price is fixed for a particular order when the order is placed. If the trigger was only defined on INSERT, it would probably be reasonable to just fetch the current price. But if you want to recalculate the subtotal of the line when a row is updated, you'd need to fetch the price as of the time the order was placed (and that assumes that you don't charge different customers different prices at the same time).
From a normalization standpoint, it also tends not to make sense to store calculated fields in the first place. It would make more sense to store the quantity and the price in the order_line table and then calculate the subtotal for the line in a view (or, if you're using 11g, as a virtual column in the table).
The mutation error does not occur because you are updating the table; it occurs because you are querying from the table that is already being updated.
If I'm understanding correctly what you want to do:
CREATE OR REPLACE TRIGGER create_subtotal
BEFORE INSERT OR UPDATE ON Order_Line
for each row
DECLARE
currentPrice products.price%TYPE;
BEGIN
-- Get the current price for the product
SELECT price INTO currentPrice FROM products WHERE product_no = :new.product_no;
-- Set the new subtotal to the current price multiplied by the order quantity
:new.subtotal := currentPrice * :new.quantity;
END;
/
(I'm unclear why you have a test for a quantity below 0, and what you want to occur in this case. If you want to set the subtotal to NULL or 0 in this case, it should be quite easy to modify the above.)

Resources