how to trigger an update on a table row value using a row value from another table in Apex Oracle SQL? - sql

i have two tables; Purchase order:
CREATE TABLE "PURCHASE_ORDER"
( "PO_NUMBER" NUMBER(*,0) NOT NULL ENABLE,
"CUSTOMER_NUMBER" NUMBER(*,0),
"PO_DATE" DATE,
"PRICE" NUMBER(*,0),
"ORDER_QUANTITY" NUMBER,
"STOCK_ID" NUMBER(*,0),
PRIMARY KEY ("PO_NUMBER")
and the bulk stock table:
CREATE TABLE "BULK_STOCK"
( "STOCK_ID" NUMBER(*,0) NOT NULL ENABLE,
"STOCK_DESCRIPTION" VARCHAR2(50),
"STOCK_UNITOF_MEASUREMENT" VARCHAR2(50),
"STOCK_STATUS" VARCHAR2(50),
"FLOOR_ID" NUMBER(*,0),
"STOCK_NAME" VARCHAR2(50),
"BULK_QUANTITY" NUMBER NOT NULL ENABLE,
PRIMARY KEY ("STOCK_ID")
I am creating a trigger that updates the BULK_QUANTITY in the bulk stock table when purchase table is inserted with values:
create or replace trigger "UPDATE_ON_PURCHASE"
BEFORE
insert or update or delete on "PURCHASE_ORDER"
for each row
begin
UPDATE bulk_stock
SET BULK_QUANTITY =BULK_QUANTITY-:old.ORDER_QUANTITY
WHERE STOCK_ID=:old.STOCK_ID;
end;
when i run this form
Nothing changes in the bulk stock table
but if I hard code it
create or replace trigger "UPDATE_ON_PURCHASE"
BEFORE
insert or update or delete on "PURCHASE_ORDER"
for each row
begin
UPDATE bulk_stock
SET BULK_QUANTITY =BULK_QUANTITY- 20 //the 20 would be the inserted ORDER_QUANTITY
WHERE STOCK_ID= 12 // 12 would be the stock_ID
it works.
This is done in Oracle Apex software. I need help with the trigger. Many thanks
end;

You're using the :OLD values in your trigger which seems problematic. On an INSERT the :OLD values are all NULL. So in the case of an INSERT, at the very least, it seems you'd want to use the :NEW values.
On an UPDATE the :OLD values contain the pre-update values. I don't know how your stock table is being maintained but it seems to me that you'd want to add the :OLD values back into stock then remove the :NEW values from stock, assuming that both the ORDER_QUANTITY and STOCK_ID can change.
When you're doing a DELETE the :OLD values contain the pre-deletion values, but the :NEW values are all NULL (makes sense, if you think about it). So in the case of a deletion it would seem that you'd want to use the :OLD values. However, if you're deleting a PO do you really want to adjust the stock? I'd think you'd need some type of status on the order to let you know if it's been fulfilled or cancelled or whatever, and only add the stock back into the bulk stock table if the order was never fulfilled.
In any case, one way to re-write your trigger would be:
create or replace trigger UPDATE_ON_PURCHASE
BEFORE insert or update or delete on PURCHASE_ORDER
for each row
begin
IF INSERTING THEN
UPDATE bulk_stock
SET BULK_QUANTITY = BULK_QUANTITY - :NEW.ORDER_QUANTITY
WHERE STOCK_ID = :NEW.STOCK_ID;
ELSIF UPDATING THEN
UPDATE BULK_STOCK
SET BULK_QUANTITY = BULK_QUANTITY + :OLD.ORDER_QUANTITY
WHERE STOCK_ID = :OLD.STOCK_ID;
UPDATE BULK_STOCK
SET BULK_QUANTITY = BULK_QUANTITY - :NEW.ORDER_QUANTITY
WHERE STOCK_ID = :NEW.STOCK_ID;
ELSIF DELETING THEN
UPDATE BULK_STOCK
SET BULK_QUANTITY = BULK_QUANTITY + :OLD.ORDER_QUANTITY
WHERE STOCK_ID = :OLD.STOCK_ID;
END IF;
end;
I'm not sure that this logic is really what you wanted as I don't understand completely what you're trying to do, particularly in the DELETE case, so take it as an example and apply whatever logic your situation calls for.
I'll also say that I think putting this logic in a trigger is a bad choice. Business logic such as this should not be implemented in a trigger - better to put it into a procedure and call the procedure when needed. Putting business logic into triggers can be problematic.
Best of luck.

Related

What is the right prototype for my SQL code?

I am new in learning SQL and I am trying to apply a trig on those two tables by using SQLcl:
prodc
( PRODC_NAME, DISCOUNTED CHAR(1) DEFAULT 'N',
PK PRIMARY KEY (PRODC_NAME));
peopleOrder
( ORDERID, PRODC_NAME,
PRIMARY KEY (ORDERID, PRODC_NAME),
FOREIGN KEY (PRODC_NAME) REFERENCES PRODC (PRODC_NAME));
I want my trig to do the following:
when I update or insert rows to peopleOrder, the trig should check if a row's product_name has value 'N' in the discounted column that is located in the product table; if it has no 'N' value, it should show an error message.
I tried many ways and the following trig, but when I insert rows, the trig seems to not be working and have no effect on the rows:
CREATE OR REPLACE TRIGGER constraint_1
After UPDATE OR INSERT on peopleOrder
for each row
begin
if not (:new.prodc_name in
(select prodc_name from prodc where discounted = 'N' )) then
RAISE_ERROR(-30001, 'No product can be ordered
!');
END IF;
INSERT INTO peopleOrder VALUES (:new.ORDERID, :new.PRODC_NAME);
END;
/
My insert command is:
INSERT INTO peopleOder (ORDERID, PRODC_NAME)
VALUES (251, 'Puton');
and 'Puton' has value 'N' in the discounted column in the prodc table.
The first thing you need is to understand what a trigger is. It is a piece of code that runs as part of the statement causing it to fire. For this reason you cannot reference the table causing it to fire. Normally that is completely unnecessary anyway. Every row trigger has access to to 2 rows: a copy of the existing data (old) and the resulting data (new). Update having both filled and Insert/Delete just new/old as appropriate for the action. Before statement triggers can modify the new data, after triggers cannot. Those rows allow you to process the columns in the table without DML or select on it. (However you cannot 'scan' the table). At any level a trigger may raise an exception which halts the entire process and the invoking statement. If no exception is raised then processing the invoking proceeds. For a more complete description see PL/SQL Language Reference or the version of Oracle you are running.
In this case your trigger breaks down to a single if
statement.
-- Revised. Orig missed that flag comming from different table.
create or replace trigger constraint_1
before update or insert on peopleOrder
for each row
declare
dis_num integer;
begin
select count(*)
into dis_num
from product
where discounted != 'N'
and product_name = :new.product_name
and rownum < 2;
if dis_num != 0 then
raise_application_error(-20001, 'No discounted product can be ordered!');
end if;
end constraint_1;
-- Orig ==============================================
CREATE OR REPLACE TRIGGER constraint_1
before UPDATE OR INSERT on peopleOrder
for each row
begin
if :new.discounted = 'N' then
RAISE_APPLICATION_ERROR(-20001, 'No discounted product can be ordered')
end if;
END;
/

How to add a constraint that checks the sum in Oracle SQL?

I'm currently doing a school project in which we need to create a database for a real estate management company. We have an OWNER table, a BUILDING table, and an OWNERSHIP table.
I want to make sure that when I enter a value for the ownership stake percentage, the sum of all ownership stakes from the various owner doesn't exceed 100%. At the moment I have no idea how to do this.
CREATE TABLE Building (
buildingID NUMBER (10) NOT NULL PRIMARY KEY,
qtyUnits NUMBER (3) NOT NULL,
landValue NUMBER (15) NOT NULL,
purchasePrice NUMBER (15) NOT NULL
);
CREATE TABLE Owners (
ownerID NUMBER (5) NOT NULL PRIMARY KEY,
lastName VARCHAR2 (50) NOT NULL,
firstName VARCHAR2 (50) NOT NULL,
telephone VARCHAR2(50) NOT NULL,
email VARCHAR2(10) NOT NULL
);
CREATE TABLE Ownership (
ownerID NUMBER (5) NOT NULL,
buildingID NUMBER (5) NOT NULL,
ownershipStake NUMBER (5,2) NOT NULL,
CONSTRAINT PK_Ownership PRIMARY KEY (ownerID,buildingID)
);
All trigger-related solutions share one problem: as soon as you have more than one user in the system, they are not enough to guarantee that the constraint is upheld. For example, if session A inserts ownershipshare of 51%, and session B inserts ownershipshare of 51%, both these inserts will succeed because neither session has committed. Then both sessions commit and you have a total ownershpshare of 102%.
One way you can get around this is with an ON COMMIT materialized view with a constraint. Unfortunately, I think materialized views are a feature available only in Oracle Enterprise Edition and not Standard or Express. I don't have an EE instance around to test with, but I think this does what you want:
create materialized view log on ownership
with primary key, rowid, sequence
( ownershipstake )
including new values;
create materialized view mv_ownership
refresh fast on commit
as
select buildingid, sum(ownershipstake) as total_ownershipstake, count(*) as count_ownershipstake
from ownership
group by buildingid;
alter materialized view mv_ownership add (
constraint ck_100 check ( total_ownershipstake <= 100 )
);
I went to a little extra work to make the materialized view fast-refreshable, so the whole thing doesn't have to be rebuilt on each commit, just the affected buildingid's.
First of all -- you could use the front-end to manage that in a separate query (i.e. limit the maximum stake by the amount left).
Should you wish to do a database check -- creating a row-level trigger on the Ownership table can help.
EDITED: adding more details
So, maybe you have already discovered that the trigger will encounter "mutating table" and are wondering "what is this guy talking about?"
OK, let me explain: this is not the complete answer to the problem.
My preferred way of dealing with this would be to use a combination of row-level AFTER trigger, an extra supplementary field in the table, and a check constraint.
Add an extra field to the Ownership table -- let's call it owned_pct
Add a check constraint on that field that says owned_pct <= 100
Create a row-level AFTER trigger that will update this value, e.g. for INSERT: update Ownership set owned_pct= nvl(owned_pct,0)+:new.ownership_pct where building_id = :new.building_id;
Note that there will be slightly different update queries for INSERT / DELETE / UPDATE cases, so make sure to test all of those
This process will try to update the owned_pct column and cause a constraint violation, which will roll back the transaction, including the initial DML statement.
Edit: Originally deleted this when I realized it was not sufficient when more than one session was involved. Undeleting to show an example of a solution that does not exhibit the "mutating table" problem. You'd have to lock the table so only one session could affect it at a time first.
You can do this with an AFTER STATEMENT trigger. That runs once per insert, update, or delete, after the entire statement is complete. That's a little sloppy, because it validates all the rows in the table, even ones that were not affected, but for your purposes is probably good enough.
create or replace trigger trig1
after insert or update on ownership
declare
l_count number;
begin
select count(*) into l_count from (
select buildingid, sum(ownershipstake)
from ownership
group by buildingid
having sum(ownershipstake) > 100
);
if l_count > 0 then
raise_application_error( -20001, 'Totals cant be over 100' );
end if;
end;
/
insert into ownership values ( 1, 1, 99 );
insert into ownership values ( 2, 1, 2 );
Error starting at line : 24 in command -
insert into ownership values ( 2, 1, 2 )
Error report -
ORA-20001: Totals cant be over 100
As I said, this validates the entire table even though I only inserted a row that affected 1 building here. So if you had a million buildings, it validates 999,999 rows unnecessarily and can have a significant performance impact.
An improved way of doing this is a compound trigger, where at the before each row timing point, you would record the building id of the row being changed. Then, at the after statement timing point, you would validate only the buildingids that had been modified.
Use compound trigger
CREATE OR REPLACE TRIGGER IVAN.trades_partial_kontrola_tg
FOR INSERT OR UPDATE OR DELETE ON ivan.trades_partial
COMPOUND TRIGGER
cNic CONSTANT NUMBER(10) := -9999999999;
--CREATE OR REPLACE TYPE IVAN.NUMBER_POLE_TYP as table of number;
lPole ivan.number_pole_typ := ivan.number_pole_typ();
lPole2 ivan.number_pole_typ;
lAmountTrades ivan.trades.amount%TYPE;
lAmountPartial ivan.trades_partial.amount%TYPE;
BEFORE EACH ROW IS
BEGIN
CASE
WHEN updating
AND :new.amount = :old.amount THEN
NULL;
WHEN nvl(:new.amount, cNic) <> nvl(:old.amount, cNic) THEN
lPole.extend();
lPole(lPole.last()) := nvl(:new.id, :old.id);
END CASE;
END BEFORE EACH ROW;
AFTER STATEMENT IS
BEGIN
SELECT DISTINCT column_value BULK COLLECT INTO lPole2 FROM TABLE(lPole);
lPole.delete;
FOR a_cur IN (SELECT * FROM TABLE(lPole2))
LOOP
SELECT t.amount INTO lAmountTrades FROM ivan.trades t WHERE t.id = a_cur.column_value;
SELECT SUM(a.amount) INTO lAmountPartial FROM ivan.trades_partial a WHERE a.id = a_cur.column_value;
IF lAmountPartial <> lAmountTrades
THEN
ivan.log_centralni_pk.myraise('Wrong amount check');
END IF;
END LOOP;
END AFTER STATEMENT;
end;

UPSERT inserts duplicate null entry into table (ORACLE)

I am trying to make an upsert trigger on ORACLE via PL/SQL by checking some examples, i am doing fine, i think it is the last step i should only configure. My requirement is that :
A system that will insert to that field will remain one column always null, so i will read column value from another table, then upsert it with inclusion of that value.
d2c_region_locale_config holds d2c_is_active value, so i firstly read that value regarding to locale condition then trigger inserts or updates to table with addition of this value on active_for_d2c column.(for update i am using locale and country columns as it is shown on where clause, they are not PK but has not null condition)
So i've created this trigger:
CREATE OR REPLACE TRIGGER BL_PIM_LOCALE_COUNTRY
BEFORE INSERT OR UPDATE ON PIM_LOCALE_COUNTRY REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
DECLARE
l_active_for_d2c INTEGER;
BEGIN
if :NEW.active_for_d2c is null then
DELETE from pim_locale_country where active_for_d2c is null;
select distinct(d2c_isactive) into l_active_for_d2c from d2c_region_locale_config where d2c_locale= :NEW.locale;
UPDATE pim_locale_country
SET locale = :NEW.locale, locale_name = :NEW.locale_name,
country = :NEW.country, country_name = :NEW.country_name, isdummy = :NEW.isdummy,
active_for_d2c = l_active_for_d2c, itextpos = :NEW.itextpos, locale_charset = :NEW.locale_charset,
fallback_locale = :NEW.fallback_locale, default_for_lang = :NEW.default_for_lang, opeclang = :NEW.opeclang
where locale = :NEW.locale and country = :NEW.country;
IF ( sql%notfound ) THEN
INSERT INTO PIM_LOCALE_COUNTRY (locale,locale_name,country,country_name,isdummy,active_for_d2c,itextpos,locale_charset,fallback_locale,default_for_lang,opeclang)
VALUES (:NEW.locale, :NEW.locale_name,:NEW.country,:NEW.country_name,:NEW.isdummy,l_active_for_d2c,:NEW.itextpos,:NEW.locale_charset,:NEW.fallback_locale,:NEW.default_for_lang,:NEW.opeclang);
END IF;
end if;
END;
It currently does the job, reads value and inserts or updates the existing locale-country couple for other values. But critical thing is that, table always has one "null" value(Please check screenshot), even that i run delete statement at the beginning on my trigger. So my question would be how to delete, or how to make this approach on trigger side ?
Many thanks for answers!
Trigger before insert doesn't block insert itself, so you insert that record twice. That is, once your trigger done its work (inserted or updated record), oracle will proceed with insert (or update) using values that stand in NEW record of your trigger. If trigger modifies NEW., it will be stored as you changed it, but if trigger inserts something itself, you can get more records.
You can use instead of insert or update triggers, and then oracle will not run its own inserts/updates after trigger finishes.
But more common way for 1-record triggers is to modify fields in NEW, for this case field NEW.d2c_is_active.
It looks like this (possible typos, please check)
CREATE OR REPLACE TRIGGER BL_PIM_LOCALE_COUNTRY
BEFORE INSERT OR UPDATE ON PIM_LOCALE_COUNTRY REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
BEGIN
if :NEW.active_for_d2c is null then
select d2c_isactive
into :NEW.active_for_d2c
from d2c_region_locale_config
where d2c_locale= :NEW.locale and rownum<=1;
end if;
END;

"Table is mutating" error occurs when inserting into a table that has an after insert trigger

I'm trying to create an after insert trigger that inserts the details of a purchase made into a purchases log table and, as well, update the total cost column in the purchases table to add GCT of 16.5% to the total. The two tables have these columns:
CREATE TABLE purchases
(
p_Id INTEGER PRIMARY KEY,
product VARCHAR(25) ,
quantity INTEGER ,
unit_Cost FLOAT ,
total_Cost FLOAT
);
CREATE TABLE purchases_log
(
event_Date DATE ,
p_Id INTEGER PRIMARY KEY,
description varchar(75)
);
The trigger I'm trying to use is:
CREATE OR REPLACE TRIGGER PURCHASES_AUDIT
AFTER INSERT ON purchases
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
DECLARE TAX FLOAT;
BEGIN
INSERT INTO purchases_log
VALUES(SYSDATE,:NEW.p_Id,'INSERT INTO PURCHASES TABLE');
tax := 1.165;
UPDATE purchases SET total_Cost = quantity * unit_Cost;
UPDATE purchases SET total_Cost = total_Cost*tax;
END PURCHASES_AUDIT;
/
however when trying to run an insert on the purchase table oracle gives me this error
ERROR at line 1:
ORA-04091: table PURCHASES is mutating, trigger/function may
not see it
ORA-06512: at "PURCHASES_AUDIT", line 9
ORA-04088: error during execution of trigger 'PURCHASES_AUDIT'
Please Help
Don't update the table on which the trigger is defined.
It sounds like you want a before insert trigger, not an after insert trigger, that modifies the :new pseudo-record. If I understand your intention correctly
CREATE OR REPLACE TRIGGER PURCHASES_AUDIT
BEFORE INSERT ON purchases
FOR EACH ROW
DECLARE
TAX FLOAT;
BEGIN
INSERT INTO purchases_log
VALUES(SYSDATE,:NEW.p_Id,'INSERT INTO PURCHASES TABLE');
tax := 1.165;
:new.total_Cost = :new.quantity * :new.unit_Cost * tax;
END PURCHASES_AUDIT;
As an aside, do you really, really want to use a float rather than a number? Do you fully understand the approximate nature of floating point numbers? I've never come across anyone working with money that wanted to use a float.
As a point of clarification: before triggers allow you to update the :new values, after triggers do not. The :new values if after triggers will always contain the final value.

ROLLBACK event triggers in postgresql

I know it may sound odd but is there any way I can call my trigger on ROLLBACK event in a table? I was going through postgresql triggers documentation, there are events only for CREATE, UPDATE, DELETE and INSERT on table.
My requirement is on transaction ROLLBACK my trigger will select last_id from a table and reset table sequence with value = last_id + 1; in short I want to preserve sequence values on rollback.
Any kind of ideas and feed back will be appreciated guys!
You can't use a sequence for this. You need a single serialization point through which all inserts have to go - otherwise the "gapless" attribute can not be guaranteed. You also need to make sure that no rows will ever be deleted from that table.
The serialization also means that only a single transaction can insert rows into that table - all other inserts have to wait until the "previous" insert has been committed or rolled back.
One pattern how this can be implemented is to have a table where the the "sequence" numbers are stored. Let's assume we need this for invoice numbers which have to be gapless for legal reasons.
So we first create the table to hold the "current value":
create table slow_sequence
(
seq_name varchar(100) not null primary key,
current_value integer not null default 0
);
-- create a "sequence" for invoices
insert into slow_sequence values ('invoice');
Now we need a function that will generate the next number but that guarantees that no two transactions can obtain the next number at the same time.
create or replace function next_number(p_seq_name text)
returns integer
as
$$
update slow_sequence
set current_value = current_value + 1
where seq_name = p_seq_name
returning current_value;
$$
language sql;
The function will increment the counter and return the incremented value as a result. Due to the update the row for the sequence is now locked and no other transaction can update that value. If the calling transaction is rolled back, so is the update to the sequence counter. If it is committed, the new value is persisted.
To ensure that every transaction uses the function, a trigger should be created.
Create the table in question:
create table invoice
(
invoice_number integer not null primary key,
customer_id integer not null,
due_date date not null
);
Now create the trigger function and the trigger:
create or replace function f_invoice_trigger()
returns trigger
as
$$
begin
-- the number is assigned unconditionally so that this can't
-- be prevented by supplying a specific number
new.invoice_number := next_number('invoice');
return new;
end;
$$
language plpgsql;
create trigger invoice_trigger
before insert on invoice
for each row
execute procedure f_invoice_trigger();
Now if one transaction does this:
insert into invoice (customer_id, due_date)
values (42, date '2015-12-01');
The new number is generated. A second transaction then needs to wait until the first insert is committed or rolled back.
As I said: this solution is not scalable. Not at all. It will slow down your application massively if there are a lot of inserts into that table. But you can't have both: a scalable and correct implementation of a gapless sequence.
I'm also pretty sure that there are edge case that are not covered by the above code. So it's pretty likely that you can still wind up with gaps.