Automatically updating a field after a manuall update in Oracle SQL - sql

I have a table that has a bunch of columns, one of which is a 'last_update_time' column. Whenever I change the value of any of the other columns, I would like 'last_update_time' to automatically update to SYSDATE.
I know that you would usually use triggers for this kind of thing, but as far as I am aware it is not possible to have a trigger for a table mutate that same table.
What is the best way of implementing the functionallity described above?

You can use a trigger before update of. You only need to specify the columns that might be updated in the clause before of
Take a look at this
SQL> create table testtrg ( c1 number, c2 date ) ;
Table created.
SQL> create or replace trigger mytrig
before update of c1
on testtrg
referencing new as new old as old
for each row
declare
begin
:new.c2 := sysdate;
end;
/ 2 3 4 5 6 7 8 9 10 11 12 13
Trigger created.
SQL> select * from testtrg;
no rows selected
SQL> insert into testtrg values ( 1 , sysdate - 360 );
1 row created.
SQL> commit;
Commit complete.
SQL> select * from testtrg ;
C1 C2
---------- ---------
1 28-JUL-19
SQL> update testtrg set c1=2 ;
1 row updated.
SQL> commit ;
Commit complete.
SQL> select * from testtrg ;
C1 C2
---------- ---------
2 22-JUL-20
SQL>

In my tables I like to keep track of when it was created in addition to when it was last modified. My triggers look something like this:
CREATE OR REPLACE TRIGGER tnsnames.tns_server_override_trg
BEFORE INSERT OR UPDATE
ON "TNSNAMES".tns_server_override
FOR EACH ROW
BEGIN
:new.modified_dt := SYSDATE;
:new.modified_by := SYS_CONTEXT( 'USERENV', 'OS_USER' );
CASE
WHEN INSERTING
THEN
:new.created_dt := :new.modified_dt;
:new.created_by := :new.modified_by;
WHEN UPDATING
THEN
:new.created_dt := :old.created_dt;
:new.created_by := :old.created_by;
END CASE;
END;

Related

Oracle trigger multiple conditions in when clause

I'm trying to create a trigger that updates a column in a table when other columns are updated. but getting the following error while saving the trigger
ORA-25000: invalid use of bind variable in trigger WHEN clause
My trigger is as follows, I'm not sure what is wrong with the code.
CREATE OR REPLACE TRIGGER Employees_ARIU
BEFORE INSERT OR UPDATE ON Employees
FOR EACH ROW
WHEN ((nvl(:OLD.EMP_SAL,0) != nvl(:NEW.EMP_SAL,0)) OR (nvl(:OLD.SAL_LEVEL,0) != nvl(:NEW.SAL_LEVEL,0)))
BEGIN
:NEW.LAST_UPDATED = SYSDATE
END;
Although IF is an alternative to WHEN, I'd say that it is better to use WHEN clause whenever possible because it is a
SQL condition that must be satisfied for the database to fire the trigger
So, why would you even let the trigger fire and then conclude that oh, OK, I don't want to do anything, after all? Better not running it at all!
Yes, WHEN clause has its restrictions and you can't put anything you want in there, but - your case isn't one of those.
(more info in Documentation, search for "WHEN clause").
So, for a sample table
SQL> create table employees
2 (id number,
3 emp_sal number,
4 sal_level number,
5 last_updated date);
Table created.
trigger would looks like this:
SQL> create or replace trigger employees_ariu
2 before insert or update on employees
3 for each row
4 when ( nvl(old.emp_sal, 0) <> nvl(new.emp_sal, 0)
5 or nvl(old.sal_level, 0) <> nvl(new.sal_level, 0)
6 )
7 begin
8 :new.last_updated := sysdate;
9 end;
10 /
Trigger created.
Testing:
SQL> insert into employees (id, emp_sal, sal_level) values (1, 100, 1);
1 row created.
SQL> select * from employees;
ID EMP_SAL SAL_LEVEL LAST_UPDATED
---------- ---------- ---------- -------------------
1 100 1 12.06.2021 12:14:17
SQL> update employees set sal_level = 2 where id = 1;
1 row updated.
SQL> select * from employees;
ID EMP_SAL SAL_LEVEL LAST_UPDATED
---------- ---------- ---------- -------------------
1 100 2 12.06.2021 12:14:33
SQL>
I think you can try updating your WHEN condition to IF statement along with few other changes -
CREATE OR REPLACE TRIGGER Employees_ARIU
BEFORE INSERT OR UPDATE ON Employees
FOR EACH ROW
BEGIN
IF ((nvl(:OLD.EMP_SAL,0) != nvl(:NEW.EMP_SAL,0)) OR (nvl(:OLD.SAL_LEVEL,0) != nvl(:NEW.SAL_LEVEL,0))) then
:NEW.LAST_UPDATED := SYSDATE;
END IF;
END;
/
Here is the fiddle.

Decrement oracle sequence when an exception occurred

I try to insert a new record in my database using pl/sql, so first of all I generate a new sequence like so:
select my_seq.nextval into seqId from dual;
And then i try to insert a new record using the generated seqId like this :
insert into myTable (id) values seqId ;
But when an error occurred during the insertion I want to decrement my sequence in an exception block. Does anyone have an idea please?
As you were already told, you shouldn't be doing this at all. Anyway, here's how you might do it.
Sample table and a sequence:
SQL> create table mytable (id number primary key);
Table created.
SQL> create sequence seq;
Sequence created.
SQL> set serveroutput on
Procedure which inserts seq.nextval into mytable and decrements the sequence in a case of a failure. I'm doing it in a simplest way - dropping it and recreating with the start parameter set to the last fetched value minus 1. DBMS_OUTPUT calls are here just to show what's going on in the procedure.
SQL> create or replace procedure p_test as
2 seqid number;
3 begin
4 seqid := seq.nextval;
5 dbms_output.put_line('Sequence number to be inserted = ' || seqid);
6
7 insert into mytable(id) values (seqid);
8
9 exception
10 when others then
11 dbms_output.put_line(sqlerrm);
12 execute immediate 'drop sequence seq';
13 execute immediate 'create sequence seq start with ' || to_char(seqid - 1);
14 end;
15 /
Procedure created.
Let's test it: this should insert 1:
SQL> exec p_test;
Sequence number to be inserted = 1
PL/SQL procedure successfully completed.
SQL> select * from mytable;
ID
----------
1
So far so good. Now, I'll manually insert ID = 2 so that the next procedure call violates unique constraint:
SQL> insert into mytable values (2);
1 row created.
Calling the procedure again:
SQL> exec p_test;
Sequence number to be inserted = 2
ORA-00001: unique constraint (SCOTT.SYS_C007547) violated
PL/SQL procedure successfully completed.
OK; procedure silently completed. It didn't insert anything, but it decremented the sequence:
SQL> select * from mytable;
ID
----------
1 --> populated with the first P_TEST call
2 --> populated manually
SQL> select seq.nextval from dual;
NEXTVAL
----------
1 --> sequence is decremented from 2 to 1
If I delete the offending ID = 2 and try again:
SQL> delete from mytable where id = 2;
1 row deleted.
SQL> exec p_test;
Sequence number to be inserted = 2
PL/SQL procedure successfully completed.
SQL> select * from mytable;
ID
----------
1
2
SQL> select seq.nextval from dual;
NEXTVAL
----------
3
SQL>
Right; kind of works, but it's not worth the pain.
Besides, as you commented that there are 20 rows in a table. What will you do if someone deletes row #13. Will you decrement all values between #14 and 20? What if that's a primary key, referenced by some foreign keys?
Seriously, don't do it.
You can not decrement after the sequence is incremented (A sequence is either incremented or decremented not both) or reinitialize the sequence (start with ...) in Oracle. There is no way of doing that. Hope this helps.
However, if you want to continue this absurdity you can try this but you need to initialize your sequence first which is myseq.nextval;
then you can first try to insert currval and if succeeds then you can increment your sequence otherwise sequence will have its previous value.
Declare
currval pls_integer;
inc pls_integer;
Begin
select seq.currval into currval from dual;
insert into myTable (id) values (currval) ;
select seq.nextval into inc from dual;
Exception
when others then
do the exception handling;
end;

IF Statement inside Trigger Clause

I want to use an if statement inside trigger but the value if comparison will come from an other select statement.
I have done the following:
create or replace
Trigger MYTRIGGER
After Insert On Table1
Referencing Old As "OLD" New As "NEW"
For Each Row
Begin
Declare Counter Int;
Select Count(*) From Table2 Where Table2."Email" = :New.U_MAIL Into Counter;
IF Counter < 1 THEN
//INSERT Statement here...
END IF;
End;
My logic is simple, if same email user exists, insert will not work.
Above code did not work. How can we do this?
A few syntax errors. Would be closer to something like this:
create or replace
Trigger MYTRIGGER
After Insert On Table1
Referencing Old As "OLD" New As "NEW"
For Each Row
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM Table2
WHERE Email = :New.U_MAIL
;
IF v_count > 0
THEN
RAISE_APPLICATION_ERROR(-20000, 'Not inserted...');
END IF;
END;
Your approach is wrong. Referential integrity should not be made using triggers, it just cannot work as required. See example:
Connected to Oracle Database 12c Enterprise Edition Release 12.1.0.2.0
Connected as test#soft12c1
SQL> create table mail_1 (email varchar2(100));
Table created
SQL> create table mail_2 (email varchar2(100));
Table created
SQL> create trigger mail_1_check
2 before insert on mail_1
3 for each row
4 declare
5 cnt integer;
6 begin
7 select count(*) into cnt from mail_2 where email = :new.email;
8 if cnt > 0 then
9 raise_application_error(-20100, 'Email already exists');
10 end if;
11 end;
12 /
Trigger created
SQL> insert into mail_2 values ('president#gov.us');
1 row inserted
SQL> insert into mail_1 values ('king#kingdom.en');
1 row inserted
SQL> insert into mail_1 values ('president#gov.us');
ORA-20100: Email already exists
ORA-06512: at "TEST.MAIL_1_CHECK", line 6
ORA-04088: error during execution of trigger 'TEST.MAIL_1_CHECK'
It looks like trigger works right, but it's not true. See what happens when several users will works simultaneously.
-- First user in his session
SQL> insert into mail_2 values ('dictator#country.by');
1 row inserted
-- Second user in his session
SQL> insert into mail_1 values ('dictator#country.by');
1 row inserted
-- First user is his session
SQL> commit;
Commit complete
-- Second user is his session
SQL> commit;
Commit complete
-- Any user in any session
SQL> select * from mail_1 natural join mail_2;
EMAIL
--------------------------------------------------------------------------------
dictator#country.by
If using triggers for this task, you should serialize any attempts to use this data, say, execute LOCK TABLE IN EXCLUSIVE MODE unless commit. Generally it's a bad decision. For this concrete task you can use much better approach:
Connected to Oracle Database 12c Enterprise Edition Release 12.1.0.2.0
Connected as test#soft12c1
SQL> create table mail_1_2nd(email varchar2(100));
Table created
SQL> create table mail_2_2nd(email varchar2(100));
Table created
SQL> create materialized view mail_check
2 refresh complete on commit
3 as
4 select 1/0 data from mail_1_2nd natural join mail_2_2nd;
Materialized view created
OK. Let's see, what if we try to use same email:
-- First user in his session
SQL> insert into mail_1_2nd values ('dictator#gov.us');
1 row inserted
-- Second user in his session
SQL> insert into mail_2_2nd values ('dictator#gov.us');
1 row inserted
SQL> commit;
Commit complete
-- First user in his session
SQL> commit;
ORA-12008: error in materialized view refresh path
ORA-01476: divisor is equal to zero
SQL> select * from mail_1_2nd natural join mail_2_2nd;
EMAIL
--------------------------------------------------------------------------------
no rows selected

Creating a sequence for a varchar2 field in Oracle

I want to create a sequence for this varchar. It would have been easier had it been a number instead of varchar. In that case, I could do
seq_no := seq_no + 1;
But what can I do when I want to store next value in column as A0000002, when the previous value was A0000001 (to increment the number in the next varchar rowby 1)?
This can be done by
to_char(seq_no,'FM0000000')
your example can be done by creating sequence in oracle
create sequence seq_no start with 1 increment by 1;
then
select 'A'||to_char(seq_no.nextval,'FM0000000') from dual;
Right now i have used in dual ..but place this
'A'||to_char(seq_no.nextval,'FM0000000')
in your required query ..this will create sequence as you mentioned
sqlfiddle
Sequences are purely numeric. However, you need a trigger anyway, so simply adapt such trigger to insert the desired prefix:
CREATE OR REPLACE TRIGGER FOO_TRG1
BEFORE INSERT
ON FOO
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
BEGIN
IF :NEW.FOO_ID IS NULL THEN
SELECT 'A' || TO_CHAR(FOO_SEQ1.NEXTVAL, 'FM0000000') INTO :NEW.FOO_ID FROM DUAL;
END IF;
END FOO_TRG1;
/
ALTER TRIGGER FOO_TRG1 ENABLE;
If you're able I'd actually use a virtual column as defined in the CREATE TABLE syntax. It makes it more easily extensible should the need arise.
Here's a working example.
SQL> create table tmp_test (
2 id number(7,0) primary key
3 , col1 number
4 , seq varchar2(8 char) generated always as (
5 'A' || to_char(id, 'FM0999999'))
6 );
Table created.
SQL>
SQL> create sequence tmp_test_seq;
Sequence created.
SQL>
SQL> create or replace trigger tmp_test_trigger
2 before insert on tmp_test
3 for each row
4 begin
5
6 :new.id := tmp_test_seq.nextval;
7 end;
8 /
Trigger created.
SQL> show errors
No errors.
SQL>
SQL> insert into tmp_test (col1)
2 values(1);
1 row created.
SQL>
SQL> select * from tmp_test;
ID COL1 SEQ
---------- ---------- --------------------------------
1 1 A0000001
Having said that; you would be better off if you did not do this unless you have an unbelievably pressing business need. There is little point to making life more difficult for yourself by prepending a constant value onto a number. As A will always be A it doesn't matter whether it's there or not.
If the format is always a letter followed by 7 digits you can do:
sequence = lpad(substr(sequence,2,7)+1,7,'0')

default value, oracle sp call

I have an oralcle SP forced on me that will not accept an empty parameter in an update. So if I wanted to set a value back to the default of ('') it will not let me pass in the empty string. Is there a keyword you can use such as default, null, etc that oracle would interpret back to the default specified for a particular column?
Sometimes things are just as simple as you hope they might be.
First, a table with a default value ...
SQL> create table t23 (
2 id number not null primary key
3 , col_d date default sysdate not null )
4 /
Table created.
SQL> insert into t23 values (1, trunc(sysdate, 'yyyy'))
2 /
1 row created.
SQL> select * from t23
2 /
ID COL_D
---------- ---------
1 01-JAN-10
SQL>
Next a procedure which updates the default column ...
SQL> create or replace procedure set_t23_date
2 ( p_id in t23.id%type
3 , p_date in t23.col_d%type )
4 is
5 begin
6 update t23
7 set col_d = p_date
8 where id = p_id;
9 end;
10 /
Procedure created.
SQL>
... but which doesn't work as we would like:
SQL> exec set_t23_date ( 1, null )
BEGIN set_t23_date ( 1, null ); END;
*
ERROR at line 1:
ORA-01407: cannot update ("APC"."T23"."COL_D") to NULL
ORA-06512: at "APC.SET_T23_DATE", line 6
ORA-06512: at line 1
SQL>
So, let's try adding a DEFAULT option ...
SQL> create or replace procedure set_t23_date
2 ( p_id in t23.id%type
3 , p_date in t23.col_d%type )
4 is
5 begin
6 if p_date is not null then
7 update t23
8 set col_d = p_date
9 where id = p_id;
10 else
11 update t23
12 set col_d = default
13 where id = p_id;
14 end if;
15 end;
16 /
Procedure created.
SQL>
... and lo!
SQL> exec set_t23_date ( 1, null )
PL/SQL procedure successfully completed.
SQL>
SQL> select * from t23
2 /
ID COL_D
---------- ---------
1 28-FEB-10
SQL>
I ran this example on an 11g database. I can't remember when Oracle introduced this exact support for DEFAULT, but it has been quite a while (9i???)
edit
The comments are really depressing. The entire point of building PL/SQL APIs is to make it easier for application developers to interact with the database. That includes being sensible enough to rewrite stored procedures when necessary. The big difference between building something out of software and, say, welding cast-iron girders together is that software is malleable and easy to change. Especially when the change doesn't alter the signature or behaviour of an existing procedure, which is the case here.
The procedure that's been forced on you:
create or replace procedure notEditable(varchar2 bar) as
begin
--update statement
null;
end;
How to use:
begin
notEditable(bar=>null);
end;
I didn't actually compile, but I believe this is the correct syntax.