Oracle DBMS INSERT Procedure not working - sql

I'm working on what should be a simple functionality for my database.
When a user signs up to my application, a field password_changed is initialised to 0, upon doing this in my user trigger I want to fire the below method to send that new user a message telling them that they should update their password.
CREATE OR REPLACE TRIGGER trg_users
BEFORE INSERT OR UPDATE ON users FOR EACH ROW
BEGIN
-- Get the new user id
IF :NEW.user_id IS NULL THEN
SELECT seq_user_id.nextval
INTO :NEW.user_id
FROM sys.dual;
END IF;
-- Alert a user that they need to change their password
IF :NEW.pass_changed IS NULL THEN
:NEW.pass_changed := 0;
send_alert(:NEW.user_id, 'Thank-you for registering, please change your password for security reasons!');
END IF;
END;
This first trigger simply initialises password set to 0 and then calls my send_alert() function, taking the :NEW.user_id which gets populated by my sequence.
send_alert() PROCEDURE:
-- Sends an alert to the user noting that they haven't changed their password
CREATE OR REPLACE PROCEDURE send_alert(
this_user users.user_id%TYPE,
this_message STRING
)
AS
BEGIN
INSERT INTO messages
VALUES('', this_user, getSystemId(), 'ALERT', this_message, '', '');
END send_alert;
When this code runs, I get the error integrity constraint (PRCSE.INBOX_MESSAGE_TO_FK) violated - parent key not found I understand what this means - however the value for the parent key should be populated via this_user, if I substitute that field for an existing value the procedure completes and INSERTS for the old user.
I can only think that for some reason the :NEW.user_id is not getting passed, but I initialise its value before calling my procedure.
EDIT 10/04/14 13:48 GMT: To clear any confusion, getSystem() returns the master admin id
-- Gets the ID of the SYSTEM user via its unique email
CREATE OR REPLACE FUNCTION getSystemId
RETURN NUMBER
IS
system_user users.user_id%TYPE;
pragma autonomous_transaction;
BEGIN
SELECT user_id
INTO system_user
FROM users
WHERE user_email = 'system#application.com'
AND user_permissions = 'ADMIN';
RETURN system_user;
COMMIT;
END getSystemId;
Any help would be appreciated -
Regards, Alex.

This is because you try to insert value in BEFORE trigger (e.g. before parent row is inserted):
SQL> create table t(x int primary key);
SQL> create table t_c(x int references t(x));
SQL> create or replace trigger tr_i
2 before insert on t
3 for each row
4 begin
5 insert into t_c(x) values(:new.x);
6 end;
7 /
SQL> insert into t values(1);
insert into t values(1)
*
error in line 1:
ORA-02291: integrity constraint (SCOTT.SYS_C00330529) voilated - parent key not found
ORA-06512: in "SCOTT.TR_I", line 2
ORA-04088: error in trigger execution 'SCOTT.TR_I'
SQL> create or replace trigger tr_i
2 after insert on t
3 for each row
4 begin
5 insert into t_c(x) values(:new.x);
6 end;
7 /
SQL> insert into t values(1);
1 row inserted.
SQL> select * from t;
X
------
1
SQL> select * from t_c;
X
------
1

Related

oracle concurrent select for update and insert

I have a table with two columns: k (primary key) and value.
I'd like to:
select for update by k, if k is not found, insert a new row with a default value.
with the returned value ( existent or new inserted row value) make some processing.
update the row and commit.
Is it possible to make this "select for update and insert default value if not found"?
If implement (1) as a select/check if found/insert if not found, we have concurrency problems, since two sessions could make the select concurrently on non existent key, both will try to insert and one of the instances will fail.
In this case the desired behavior is to perform atomically the select/insert and one of the instance perform it and the second one keep locked until the first one commits, and then use the value inserted by the first one.
We implement it always doing an "insert ... if not exist.../commit" before the "select for update" but this implies always trying to insert when it is a unlikely needed.
Is there any way to implement it on one sql sentence?
Thanks!!
See if k is available
SELECT * FROM table
WHERE k = value
FOR UPDATE
If no rows returned, then it doesn't exist. Insert it:
INSERT INTO table(k, col1, col2)
VALUES (value, val1, default))
select ... for update is the first step you should make; without it, you can't "reserve" that row for further processing (unless you're willing to lock the whole table in exclusive mode; if that "processing" takes no time, that could also be an option, especially if there are not many users who will be doing it).
If row exists, the rest is simple - process it, update it, commit.
But, if it doesn't exist, you'll have to insert a new row (just as you said), and here's a problem of two (or more) users inserting the same value.
To avoid it, create a function which
will return unique ID value for a new row
is an autonomous transaction
why? Because you're performing DML in it (update or insert), and you can't do that in a function unless it is an autonomous transaction
Users will have to use that function to get the next ID value. Here's an example: you'll need a table (my_id) which holds the last used ID (and every user who accesses it via the function will create a new value).
Table:
SQL> create table my_id (id number);
Table created.
Function:
SQL> create or replace function f_id
2 return number
3 is
4 pragma autonomous_transaction;
5 l_nextval number;
6 begin
7 select id + 1
8 into l_nextval
9 from my_id
10 for update of id;
11
12 update my_id set
13 id = l_nextval;
14
15 commit;
16 return (l_nextval);
17
18 exception
19 when no_data_found then
20 lock table my_id in exclusive mode;
21
22 insert into my_id (id)
23 values (1);
24
25 commit;
26 return(1);
27 end;
28 /
Function created.
Use it as
SQL> select f_id from dual;
F_ID
----------
1
SQL>
That's it ... code you'll use will then be something like this:
SQL> create table test
2 (id number constraint pk_test primary key,
3 name varchar2(10),
4 datum date
5 );
Table created.
SQL> create or replace procedure p_test (par_id in number)
2 is
3 l_id test.id%type;
4 begin
5 select id
6 into l_id
7 from test
8 where id = par_id
9 for update;
10
11 update test set datum = sysdate where id = par_id;
12 exception
13 when no_data_found then
14 insert into test (id, name, datum)
15 values (f_id, 'Little', sysdate); --> function call is here
16 end;
17 /
Procedure created.
SQL> exec p_test (1);
PL/SQL procedure successfully completed.
SQL> select * from test;
ID NAME DATUM
---------- ---------- -------------------
1 Little 04.09.2021 20:49:21
SQL> exec p_test (1);
PL/SQL procedure successfully completed.
SQL> select * from test;
ID NAME DATUM
---------- ---------- -------------------
1 Little 04.09.2021 20:49:21 --> row was inserted
SQL> exec p_test (1);
PL/SQL procedure successfully completed.
SQL> select * from test;
ID NAME DATUM
---------- ---------- -------------------
1 Little 04.09.2021 20:49:30 --> row was updated
SQL>
Use a sequence to generate a surrogate primary key instead of using a natural key. If you had a real natural key, then it would be extremely unlikely for two users to submit the same value at the same time.
There are several ways to automatically generate primary keys. I prefer to use sequence defaults, like this:
create sequence test_seq;
create table test1
(
k number default test_seq.nextval,
value varchar2(4000),
constraint test1_pk primary key(k)
);
If you can't switch to a surrogate key or a real natural key:
Change the "insert ... if not exist.../commit" to a simpler "insert ... if not exist", and perform all operations in a single transaction. Inserting the same primary key in different sessions, even uncommitted, is impossible in Oracle. Although the SELECT won't cause a block, the INSERT will. This behavior is an exception to Oracle's implementation of isolation, the "I" in "ACID", and in this case that behavior can work to your advantage.
If two sessions attempt to insert the same primary key at the same time, the second session will hang, and when the first session finally commits, the second session will fail with the exception "ORA-00001: unique constraint (X.Y) violated". Let that exception become your flag for knowing when a user has submitted a duplicate value. You can catch the exception in the application and have the user try again.

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;

trigger in oracle that ensures that the value of ExpDate from the table ExpIt is less than or equal to the ERSubDate from the ExpReport

I am trying to create a trigger in oracle that ensures that the value of ExpDate from the table ExpIt is less than or equal to the ERSubDate from the ExpReport on INSERT and UPDATE statements that change the ExpDate in the ExpIt table.
When ran in the command prompt, the following comes up
warning: Trigger created with compilation errors.
Here is what I have tried so far, where am I going wrong?
Thank you in advance.
CREATE OR REPLACE TRIGGER Expense_Date
BEFORE INSERT OR UPDATE OF ExpDate
ON ExpIt
FOR EACH ROW
DECLARE
anExpDate ExpIts.ExpDate%TYPE;
anERSubDate ExpReport.ERSubDate%TYPE;
DateError EXCEPTION;
ExMessage VARCHAR(200);
BEGIN
SELECT ExpDate, ERSubDate
INTO anExpDate, anERSubDate
FROM ExpIt, ExpReport
WHERE ExpIt.ExpDate = :NEW.ExpDate;
IF anExpDate <= anERSubDate THEN
RAISE DateError;
END IF;
EXCEPTION
WHEN DateError THEN
ExMessage := ExMessage || 'Expense Date is Incorrect as it is after the Expense Report Submition date' ||
to_date(anExpDate);
raise_application_error(-20001, ExMessage);
END;
/
Before you go too far down this track - be aware that you generally cannot access the table you are triggering on from within the trigger itself.
In your case, your trigger is on EXPIT and you want to query EXPIT. That won't work.
Here's a trivial example of that:
SQL> create table t (x int );
Table created.
SQL> insert into t values (1);
1 row created.
SQL> commit;
Commit complete.
SQL>
SQL> create or replace
2 trigger TRG
3 before insert on T
4 for each row
5 declare
6 blah int;
7 begin
8 select count(*) into blah from t;
9 end;
10 /
Trigger created.
SQL>
SQL> insert into t values (2);
1 row created.
It looks fine, but in reality, there are plenty of cases where it will NOT work
SQL> insert into t
2 select rownum from dual
3 connect by level <= 5;
insert into t
*
ERROR at line 1:
ORA-04091: table MCDONAC.T is mutating, trigger/function may not see it
ORA-06512: at "MCDONAC.TRG", line 4
ORA-04088: error during execution of trigger 'MCDONAC.TRG'
This is a big topic, and more details on the issue and how to work around it are here
https://asktom.oracle.com/pls/apex/asktom.search?file=MutatingTable.html#presentation-downloads-reg

Insert value on second trigger with value from first trigger PL\SQL

Hi I try to Insert value in the second trigger with new id from first trigger only if condition is fulfiled, but I'm stuck.
table1_trg works
CREATE TABLE table1 (
id NUMBER(9,0) NOT NULL,
subject VARCHAR2(200) NOT NULL,
url_address VARCHAR2(200) NOT NULL,
)
CREATE OR REPLACE TRIGGER table1_trg
BEFORE INSERT ON table1
FOR EACH ROW
BEGIN
SELECT table1_seq.NEXTVAL
INTO :new.id
FROM dual;
END;
/
CREATE OR REPLACE TRIGGER table1_url
BEFORE INSERT ON table1
FOR EACH ROW
WHEN (NEW.subject = 'Task')
BEGIN
INSERT INTO CSB.table1 (url_address)
VALUES ('blabla.com?' || :new.id);
END;
/
I insert only subject but after that i receive exception that subject can not be null.
INSERT INTO corp_tasks_spec (subject) VALUES ('Task')
Any ideas how to resolve it?
You should not be inserting a new record into the same table, you should be modifying the column values for the row you're already inserting - which the trigger is firing against. You're getting the error because of that second insert - which is only specifying the URL value, not the subject or ID (though the first trigger would fire again and set the ID for that new row as well - so it complains about the subject).
Having two triggers on the same firing point can be difficult in old versions of Oracle as the order they fired wasn't guaranteed - so for instance your second trigger might fire before the first, and ID hasn't been set yet. You can control the order in later versions (from 11g) with FOLLOWS:
CREATE OR REPLACE TRIGGER table1_url
BEFORE INSERT ON table1
FOR EACH ROW
FOLLOWS table1_trg
WHEN (NEW.subject = 'Task')
BEGIN
:NEW.url_address := 'blabla.com?' || :new.id;
END;
/
This now fires after the first trigger, so ID is set, and assigns a value to the URL in this row rather than trying to create another row:
INSERT INTO table1 (subject) VALUES ('Task');
1 row inserted.
SELECT * FROM table1;
ID SUBJECT URL_ADDRESS
---------- ---------- --------------------
2 Task blabla.com?2
But you don't really need two triggers here, you could do:
DROP TRIGGER table1_url;
CREATE OR REPLACE TRIGGER table1_trg
BEFORE INSERT ON table1
FOR EACH ROW
BEGIN
:NEW.id := table1_seq.NEXTVAL; -- no need to select from dual in recent versions
IF :NEW.subject = 'Task' THEN
:NEW.url_address := 'blabla.com?' || :new.id;
END IF;
END;
/
Then that trigger generates the ID and sets the URL:
INSERT INTO table1 (subject) VALUES ('Task');
1 row inserted.
SELECT * FROM table1;
ID SUBJECT URL_ADDRESS
---------- ---------- --------------------
2 Task blabla.com?2
3 Task blabla.com?3
Of course, for anything except Task you'll have to specify the URL as part of the insert, or it will error as that is a not-null column.
Create sequence
CREATE SEQUENCE table1_SEQ
START WITH 1
MAXVALUE 100000
MINVALUE 1
NOCYCLE
NOCACHE
NOORDER;
CREATE TRIGGER
CREATE OR REPLACE TRIGGER table1_TRG
Before Insert
ON table1 Referencing New As New Old As Old
For Each Row
Declare V_Val Number;
Begin
Select table1_SEQ.NextVal into V_Val From Dual;
If Inserting Then
:New.id:= V_Val;
End If;
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