create a trigger to update values - oracle

I have these two tables
Project (projID,TotArticles)
Article (prodID,ArticleID)
How can I create a trigger to update by 1 the total amount of article every time someone published an article on it?
CREATE TRIGGER Art_Up
AFTER INSERT ON Article
FOR EACH ROW
UPDATE Project
SET TotArticle = TotArticle + 1
WHERE paperID = NEW.PaperID;
However, it gives me this error PLS-00103: Encountered the symbol ";"

You messed some names, once you write projID, once prodID, and in your trigger it is paperID. Also trigger has no begin ... end;. And you did not handle adding articles where projID does not exist in table project. You could check it at first or use rowcount after update and if it is 0 then use insert. More simple is to use merge.
create or replace trigger art_up after insert on article for each row
begin
merge into project
using (select :new.projid projid from dual) src
on (project.projid = src.projid)
when matched then update set totarticles = totarticles + 1
when not matched then insert (projID, TotArticles) values (:new.projid, 1);
end;
It works, I tested some basic inserts, but it is not recommended at all, because:
it's a bad idea to keep logic in triggers,
triggers can be droppped, disabled and then this information may be misleading, false,
we are slowing insert operations,
this trigger does not handle delete where you should decrement total number of articles.
Instead of trigger use simple view:
create or replace view vw_project as select projID, count(1) total from article group by projid;

Related

Insert the data by running trigger but after running the condition in Oracle

I am trying to insert data in the form of creating trigger. But before inserting the data into the table, I want to run one condition which is
want to delete the data with some condition. So I implemented like this below
Once the condition is true then only INSERT otherwise not.
create or replace TRIGGER APP_WFM.TRG_INS_NE_SF_SITE_INSTANCE
BEFORE INSERT OR UPDATE ON SF_NE_DETAILS
FOR EACH ROW
BEGIN
IF
DELETE FROM NE_SITE_INSTANCE
WHERE build_by IN ('RCOM','RJIL','IP1','IP1 COLO')
AND validation_status IS NULL
AND wfm_hoto_flag IS NULL;
THEN
INSERT INTO NE_SITE_INSTANCE (
rf_siteid,
sap_id,
sitename,
DSUPPLIERBATTERYBANK,
DUTILITYINSTBATTERYBANK,
DENDDGDATE,
DSTARTDGDATE,
DEMFEND,
DEMFSTART,
DQUALITYDATE,
DRFE1OFFERED,
DUTILITYINSTODC,
DSUPPLIERSMPS,
DUTILITYINSTSMPS,
DLTOWERPOLEMATERIAL,
DLNOOFPLATFORM,
DLNOOFPOLES,
DLNOOFSECTORS,
DLBATREDIFIER,
DLNOOFREDINSMPS,
FUBATCOMMREPORT,
FUDGCOMREPORT,
FUSMPSCOMREPORT,
DLSHELTER,
CANDIDATEID,
CIRCLE,
CITY_NAME,
LATITUDE,
LONGITUDE,
DLMATERIALSUPPLIER,
DLBATMODELNAME,
DBATRECTIFIERSRNO1,
DBATRECTIFIERSRNO2,
DBATRECTIFIERSRNO3,
DBATRECTIFIERSRNO4,
DBATRECTIFIERSRNO5,
DBATRECTIFIERSRNO6,
DCBMS,
DDGALTERNATEMAKE,
DGALTERNATESRNO,
DCRANKMAKE,
DCRANKSRNO,
DDCENGINEMAKE,
DDGENGINESRNO,
DLDGMODELNAME,
DLEARTHING,
DLRP1MODELNAME,
DCENERGYMETEROWNSRNO,
DLMODELFDPFDMS1_COUNT,
DLMODELFDPFDMS1,
DFDPMAKE,
DLMODELFDPFDMS_COUNT,
DLMODELFDPFDMS,
DODCMAKE,
DLDOCMODELNAME,
ODCSERIALNO,
DLGO,
DSHELTERMAKE,
DSHELTERSRNO,
DCONTROLLERADDRESS,
DSMPSMAKE,
DLMODELSMPS,
DPAUIPADDRESS,
DSMPSRECTIFIERSRNO1,
DSMPSRECTIFIERSRNO2,
DSMPSRECTIFIERSRNO3,
DSMPSRECTIFIERSRNO4,
DSMPSRECTIFIERSRNO5,
DSMPSRECTIFIERSRNO6,
DSMPSSRNO,
DLLTOWERTYPE,
DSUPPLIERFDMS,
DRFE1DECLARED,
SITE_TYPE,
DSMPSRECTIFIERSRNO7,
DSMPSRECTIFIERSRNO8,
DSMPSRECTIFIERSRNO9,
DSMPSRECTIFIERSRNO10,
DSMPSRECTIFIERSRNO11,
DSMPSRECTIFIERSRNO12,
ALARM_GATEWAY_MAKE,
ALARM_GATEWAY_MODEL_NAME,
ALRM_GTWAY_INTS_DT,
ALRM_GTWAY_SR_NO,
ALRM_GTWAY_COMM_DT
) VALUES (
:NEW.rf_siteid,
:NEW.sap_id,
:NEW.site_name,
:NEW.DSUPPLIERBATTERYBANK,
:NEW.DUTILITYINSTBATTERYBANK,
:NEW.DENDDGDATE,
:NEW.DSTARTDGDATE,
:NEW.DEMFEND,
:NEW.DEMFSTART,
:NEW.DQUALITYDATE,
:NEW.DRFE1OFFERED,
:NEW.DUTILITYINSTODC,
:NEW.DSUPPLIERSMPS,
:NEW.DUTILITYINSTSMPS,
:NEW.DLTOWERPOLEMATERIAL,
:NEW.DLNOOFPLATFORM,
:NEW.DLNOOFPOLES,
:NEW.DLNOOFSECTORS,
:NEW.DLBATREDIFIER,
:NEW.DLNOOFREDINSMPS,
:NEW.FUBATCOMMREPORT,
:NEW.FUDGCOMREPORT,
:NEW.FUSMPSCOMREPORT,
:NEW.DLSHELTER,
:NEW.CANDIDATEID,
:NEW.CIRCLE,
:NEW.CITY_NAME,
:NEW.LATITUDE,
:NEW.LONGITUDE,
:NEW.DLMATERIALSUPPLIER,
:NEW.DLBATMODELNAME,
:NEW.DBATRECTIFIERSRNO1,
:NEW.DBATRECTIFIERSRNO2,
:NEW.DBATRECTIFIERSRNO3,
:NEW.DBATRECTIFIERSRNO4,
:NEW.DBATRECTIFIERSRNO5,
:NEW.DBATRECTIFIERSRNO6,
:NEW.DCBMS,
:NEW.DDGALTERNATEMAKE,
:NEW.DGALTERNATESRNO,
:NEW.DCRANKMAKE,
:NEW.DCRANKSRNO,
:NEW.DDCENGINEMAKE,
:NEW.DDGENGINESRNO,
:NEW.DLDGMODELNAME,
:NEW.DLEARTHING,
:NEW.DLRP1MODELNAME,
:NEW.DCENERGYMETEROWNSRNO,
:NEW.DLMODELFDPFDMS1_COUNT,
:NEW.DLMODELFDPFDMS1,
:NEW.DFDPMAKE,
:NEW.DLMODELFDPFDMS_COUNT,
:NEW.DLMODELFDPFDMS,
:NEW.DODCMAKE,
:NEW.DLDOCMODELNAME,
:NEW.ODCSERIALNO,
:NEW.DLGO,
:NEW.DSHELTERMAKE,
:NEW.DSHELTERSRNO,
:NEW.DCONTROLLERADDRESS,
:NEW.DSMPSMAKE,
:NEW.DLMODELSMPS,
:NEW.DPAUIPADDRESS,
:NEW.DSMPSRECTIFIERSRNO1,
:NEW.DSMPSRECTIFIERSRNO2,
:NEW.DSMPSRECTIFIERSRNO3,
:NEW.DSMPSRECTIFIERSRNO4,
:NEW.DSMPSRECTIFIERSRNO5,
:NEW.DSMPSRECTIFIERSRNO6,
:NEW.DSMPSSRNO,
:NEW.DLLTOWERTYPE,
:NEW.DSUPPLIERFDMS,
:NEW.DRFE1DECLARED,
:NEW.SITE_TYPE,
:NEW.DSMPSRECTIFIERSRNO7,
:NEW.DSMPSRECTIFIERSRNO8,
:NEW.DSMPSRECTIFIERSRNO9,
:NEW.DSMPSRECTIFIERSRNO10,
:NEW.DSMPSRECTIFIERSRNO11,
:NEW.DSMPSRECTIFIERSRNO12,
:NEW.ALARM_GATEWAY_MAKE,
:NEW.ALARM_GATEWAY_MODEL_NAME,
:NEW.ALRM_GTWAY_INTS_DT,
:NEW.ALRM_GTWAY_SR_NO,
:NEW.ALRM_GTWAY_COMM_DT
);
END;
But its giving error as
Error(8,1): PLS-00103: Encountered the symbol "DELETE" when expecting one of the following: ( - + case mod new not null continue avg count current exists max min prior sql stddev sum variance execute forall merge time timestamp interval date pipe <an alternat
If the goal is to check whether the delete removed rows before running the insert, you'd want something like
create or replace TRIGGER APP_WFM.TRG_INS_NE_SF_SITE_INSTANCE
BEFORE INSERT OR UPDATE ON SF_NE_DETAILS
FOR EACH ROW
BEGIN
DELETE ...
IF( sql%rowcount > 0 )
THEN
INSERT ...
END IF;
END;
It's not obvious to me that this makes a whole lot of sense, though. If you run a SQL statement that inserts 1000 rows into SF_NE_DETAILS, you'd run the delete 1000 times (which seems inefficient) and insert only a single row into NE_SITE_INSTANCE. That seems unlikely to be what you really want. My guess is that you really want a statement-level trigger that does the delete
create or replace TRIGGER APP_WFM.TRG_INS_NE_SF_SITE_INSTANCE
BEFORE INSERT OR UPDATE ON SF_NE_DETAILS
BEGIN
DELETE ...
END;
and then the row-level trigger that just does the insert. Then in the case that you run an insert that inserts 1000 rows, you'd run the delete once and the insert 1000 times.
Of course, this assumes that there is a good reason that you need to have two tables that seem to consist of mostly duplicate information. That would make some sense if ne_site_instance was a history table but that doesn't appear to be what's going on here. Are you sure that you don't really want a view/ materialized view/ something else?

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;

PL/SQL Update Trigger Updates All Rows

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.

UPDATE on INSERT duplicate primary key in Oracle?

I have a simple INSERT query where I need to use UPDATE instead when the primary key is a duplicate. In MySQL this seems easier, in Oracle it seems I need to use MERGE.
All examples I could find of MERGE had some sort of "source" and "target" tables, in my case, the source and target is the same table. I was not able to make sense of the examples to create my own query.
Is MERGE the only way or maybe there's a better solution?
INSERT INTO movie_ratings
VALUES (1, 3, 5)
It's basically this and the primary key is the first 2 values, so an update would be like this:
UPDATE movie_ratings
SET rating = 8
WHERE mid = 1 AND aid = 3
I thought of using a trigger that would automatically execute the UPDATE statement when the INSERT was called but only if the primary key is a duplicate. Is there any problem doing it this way? I need some help with triggers though as I'm having some difficulty trying to understand them and doing my own.
MERGE is the 'do INSERT or UPDATE as appropriate' statement in Standard SQL, and probably therefore in Oracle SQL too.
Yes, you need a 'table' to merge from, but you can almost certainly create that table on the fly:
MERGE INTO Movie_Ratings M
USING (SELECT 1 AS mid, 3 AS aid, 8 AS rating FROM dual) N
ON (M.mid = N.mid AND M.aid = N.aid)
WHEN MATCHED THEN UPDATE SET M.rating = N.rating
WHEN NOT MATCHED THEN INSERT( mid, aid, rating)
VALUES(N.mid, N.aid, N.rating);
(Syntax not verified.)
A typical way of doing this is
performing the INSERT and catch a DUP_VAL_ON_INDEX and then perform an UPDATE instead
performing the UPDATE first and if SQL%Rows = 0 perform an INSERT
You can't write a trigger on a table that does another operation on the same table. That's causing an Oracle error (mutating tables).
I'm a T-SQL guy but a trigger in this case is not a good solution. Most triggers are not good solutions. In T-SQL, I would simply perform an IF EXISTS (SELECT * FROM dbo.Table WHERE ...) but in Oracle, you have to select the count...
DECLARE
cnt NUMBER;
BEGIN
SELECT COUNT(*)
INTO cnt
FROM mytable
WHERE id = 12345;
IF( cnt = 0 )
THEN
...
ELSE
...
END IF;
END;
It would appear that MERGE is what you need in this case:
MERGE INTO movie_ratings mr
USING (
SELECT rating, mid, aid
WHERE mid = 1 AND aid = 3) mri
ON (mr.movie_ratings_id = mri.movie_ratings_id)
WHEN MATCHED THEN
UPDATE SET mr.rating = 8 WHERE mr.mid = 1 AND mr.aid = 3
WHEN NOT MATCHED THEN
INSERT (mr.rating, mr.mid, mr.aid)
VALUES (1, 3, 8)
Like I said, I'm a T-SQL guy but the basic idea here is to "join" the movie_rating table against itself. If there's no performance hit on using the "if exists" example, I'd use it for readability.

How to? Correct sql syntax for finding the next available identifier

I think I could use some help here from more experienced users...
I have an integer field name in a table, let's call it SO_ID in a table SO, and to each new row I need to calculate a new SO_ID based on the following rules
1) SO_ID consists of 6 letters where first 3 are an area code, and the last three is the sequenced number within this area.
309001
309002
309003
2) so the next new row will have a SO_ID of value
309004
3) if someone deletes the row with SO_ID value = 309002, then the next new row must recycle this value, so the next new row has got to have the SO_ID of value
309002
can anyone please provide me with either a SQL function or PL/SQL (perhaps a trigger straightaway?) function that would return the next available SO_ID I need to use ?
I reckon I could get use of keyword rownum in my sql, but the follwoing just doens't work properly
select max(so_id),max(rownum) from(
select (so_id),rownum,cast(substr(cast(so_id as varchar(6)),4,3) as int) from SO
where length(so_id)=6
and substr(cast(so_id as varchar(6)),1,3)='309'
and cast(substr(cast(so_id as varchar(6)),4,3) as int)=rownum
order by so_id
);
thank you for all your help!
This kind of logic is fraught with peril. What if two sessions calculate the same "next" value, or both try to reuse the same "deleted" value? Since your column is an integer, you'd probably be better off querying "between 309001 and 309999", but that begs the question of what happens when you hit the thousandth item in area 309?
Is it possible to make SO_ID a foreign key to another table as well as a unique key? You could pre-populate the parent table with all valid IDs (or use a function to generate them as needed), and then it would be a simple matter to select the lowest one where a child record doesn't exist.
well, we came up with this... sort of works.. concurrency is 'solved' via unique constraint
select min(lastnumber)
from
(
select so_id,so_id-LAG(so_id, 1, so_id) OVER (ORDER BY so_id) AS diff,LAG(so_id, 1, so_id) OVER (ORDER BY so_id)as lastnumber
from so_miso
where substr(cast(so_id as varchar(6)),1,3)='309'
and length(so_id)=6
order by so_id
)a
where diff>1;
Do you really need to compute & store this value at the time a row is inserted? You would normally be better off storing the area code and a date in a table and computing the SO_ID in a view, i.e.
SELECT area_code ||
LPAD( DENSE_RANK() OVER( PARTITION BY area_code
ORDER BY date_column ),
3,
'0' ) AS so_id,
<<other columns>>
FROM your_table
or having a process that runs periodically (nightly, for example) to assign the SO_ID using similar logic.
If your application is not pure sql, you could do this in application code (ie: Java code). This would be more straightforward.
If you are recycling numbers when rows are deleted, your base table must be consulted when generating the next number. "Legacy" pre-relational schemes that attempt to encode information in numbers are a pain to make airtight when numbers must be recycled after deletes, as you say yours must.
If you want to avoid having to scan your table looking for gaps, an after-delete routine must write the deleted number to a separate table in a "ReuseMe" column. The insert routine does this:
begins trans
selects next-number table for update
uses a reuseme number if available else uses the next number
clears the reuseme number if applicable or increments the next-number in the next-number table
commits trans
Ignoring the issues about concurrency, the following should give a decent start.
If 'traffic' on the table is low enough, go with locking the table in exclusive mode for the duration of the transaction.
create table blah (soc_id number(6));
insert into blah select 309000 + rownum from user_tables;
delete from blah where soc_id = 309003;
commit;
create or replace function get_next (i_soc in number) return number is
v_min number := i_soc* 1000;
v_max number := v_min + 999;
begin
lock table blah in exclusive mode;
select min(rn) into v_min
from
(select rownum rn from dual connect by level <= 999
minus
select to_number(substr(soc_id,4))
from blah
where soc_id between v_min and v_max);
return v_min;
end;

Resources