How to create a trigger to allow both manual and auto primary key in sql developer with oracle 11g - sql

I am attempting to create a trigger (using SQL Developer with Oracle 11g) that will allow manual insertions onto the primary key, and if a record is created without a specified primary key it will assign one from a sequence. First I tried to use a select statement in the trigger that checks if the id generated by the sequence is already in the table because of manual insertion :
DROP TABLE testing;
DROP SEQUENCE testing_seq;
CREATE TABLE testing (
id_number NUMBER PRIMARY KEY,
test_data VARCHAR(50)
);
CREATE SEQUENCE testing_seq
MINVALUE 1
MAXVALUE 10000
START WITH 1
INCREMENT BY 1
NOORDER
NOCYCLE;
CREATE OR REPLACE TRIGGER auto_testing_id BEFORE
INSERT ON testing
FOR EACH ROW
DECLARE
tmp NUMBER;
seq NUMBER;
BEGIN
IF :NEW.id_number IS NULL THEN
seq := testing_seq.nextval;
SELECT
(SELECT 1 FROM testing WHERE id_number = seq
) INTO tmp FROM dual;
while (tmp = 1)
loop
seq := testing_seq.nextval;
SELECT
(SELECT 1 FROM testing WHERE id_number = seq
) INTO tmp FROM dual;
END loop;
:NEW.id_number := seq;
END IF;
END;
/
INSERT INTO testing VALUES(1,'test1');
INSERT INTO testing (test_data) VALUES('test2');
SELECT * FROM testing;
Table TESTING dropped.
Sequence TESTING_SEQ dropped.
Table TESTING created.
Sequence TESTING_SEQ created.
Trigger AUTO_TESTING_ID compiled
1 row inserted.
1 row inserted.
ID_NUMBER TEST_DATA
---------- --------------------------------------------------
1 test1
2 test2
This works for manually created insertions, but not if I try to insert using a select statement. I believe this is because I am referencing the table being inserted on inside the trigger.
I tried a trigger without the check, but as expected if the trigger created an id that was already in the table it threw a unique constraint error
CREATE OR REPLACE TRIGGER auto_testing_id2 BEFORE
INSERT ON testing
FOR EACH ROW
DECLARE
BEGIN
IF :NEW.id_number is null
then
:NEW.id_number := testing_seq.nextval;
end if;
end;
/
Trigger AUTO_TESTING_ID2 compiled
1 row inserted.
Error starting at line : 59 in command -
INSERT INTO testing (test_data) VALUES('test2')
Error report -
SQL Error: ORA-00001: unique constraint (KATRINA_LEARNING.SYS_C001190313) violated
00001. 00000 - "unique constraint (%s.%s) violated"
*Cause: An UPDATE or INSERT statement attempted to insert a duplicate key.
For Trusted Oracle configured in DBMS MAC mode, you may see
this message if a duplicate entry exists at a different level.
*Action: Either remove the unique restriction or do not insert the key.
ID_NUMBER TEST_DATA
---------- --------------------------------------------------
1 test1
I tried to catch this error (using error name DUP_VAL_ON_INDEX), and then loop it until it found the next number in the sequence that isn't in the table (with and without error catching), but it wouldn't even send up a test error message, and when I added the loop it wouldn't compile...
Can anyone please help me create a trigger that works without using a select statement to see if the sequence nextval is already used?

The example I'm adding here is clunky and performance here would be poor, but I wanted to put out an idea that could perhaps be a starting place.
You mentioned that you don't want to SELECT to check whether NEXTVAL is already used. If you meant that you didn't want to have to perform any SELECT at all, then this answer would be cheating as it does include a SELECT statement. But if you only want to avoid the mutating-table problem, then it could be a possibility.
The approach I'll add here takes a few steps to avoid collision, and runs them anytime a non-NULL value is provided as a key. It is currently set up to fire per-row, but if entire statements will be all-NULL or all-non-NULL then it could possible be changed to a statement- or compound-trigger to improve efficiency a little. In any event, the collision-avoidance is inefficient in this example.
General steps:
- If NULL, just use the NEXTVAL
- If non-NULL, check the LAST_VALUE and CACHE for the SEQUENCE against the provided value.
- If the provided value is within the CACHE (or beyond the cache) and could cause a collision, jump the SEQUENCE beyond the provided value and throw away the values in the cache.
Create the test table/sequence:
CREATE TABLE MY_TABLE (
MY_TABLE_ID NUMBER NOT NULL PRIMARY KEY
);
--Default cache here 20
CREATE SEQUENCE MY_SEQUENCE;
Create an autonomous synchronizer. Please note, this is not at all efficient, and concurrency could be a real problem (A serializing alternative is below).
It assumes a CACHE of at least 1 and may be incompatible with NOCACHE. (Actually the whole situation might be simpler with a NOCACHE SEQUENCE though)
CREATE OR REPLACE PROCEDURE SYNC_MY_SEQUENCE(P_CANDIDATE_MAX_VALUE IN NUMBER)
IS
PRAGMA AUTONOMOUS_TRANSACTION;
V_LAST_NUMBER NUMBER;
V_CACHE_SIZE NUMBER;
V_SEQUENCE_GAP NUMBER;
V_SEQUENCE_DELTA NUMBER;
V_NEXTVAL NUMBER;
BEGIN
SELECT
LAST_NUMBER,
CACHE_SIZE
INTO V_LAST_NUMBER, V_CACHE_SIZE
FROM USER_SEQUENCES
WHERE SEQUENCE_NAME = 'MY_SEQUENCE';
--Only do anything if the provided value could cause a collision.
IF P_CANDIDATE_MAX_VALUE >= (V_LAST_NUMBER - V_CACHE_SIZE)
THEN
-- Get the delta, in case the provided value is way way higher than the SEQUENCE
V_SEQUENCE_DELTA := P_CANDIDATE_MAX_VALUE + V_CACHE_SIZE - V_LAST_NUMBER ;
-- Use the biggest gap to get a safe zone when resetting the SEQUENCE
V_SEQUENCE_GAP := GREATEST(V_SEQUENCE_DELTA, V_CACHE_SIZE);
-- Set the increment so the distance between the last_value and the safe zone can be moved in one jump
EXECUTE IMMEDIATE 'ALTER SEQUENCE MY_SEQUENCE INCREMENT BY '||V_SEQUENCE_GAP;
-- Jump to the safe zone.
V_NEXTVAL := MY_SEQUENCE.NEXTVAL;
-- Reset increment. Note there is a space here that other sessions could get big NEXTVALs from concurrent access
EXECUTE IMMEDIATE 'ALTER SEQUENCE MY_SEQUENCE INCREMENT BY 1';
--Chew through the rest of at least one cache cycle.
FOR CACHE_POINTER IN 1..V_CACHE_SIZE LOOP
V_NEXTVAL := MY_SEQUENCE.NEXTVAL;
END LOOP;
END IF;
COMMIT;
END;
/
EDIT: it would be even more costly, but one might be able to serialize access to manage concurrency with something like the below alternative:
CREATE OR REPLACE PROCEDURE SYNC_MY_SEQUENCE(P_CANDIDATE_MAX_VALUE IN NUMBER)
IS
PRAGMA AUTONOMOUS_TRANSACTION;
V_LAST_NUMBER NUMBER;
V_CACHE_SIZE NUMBER;
V_SEQUENCE_GAP NUMBER;
V_SEQUENCE_DELTA NUMBER;
V_NEXTVAL NUMBER;
V_LOCK_STATUS NUMBER;
V_LOCK_HANDLE VARCHAR2(64);
C_LOCK_KEY CONSTANT VARCHAR2(20) := 'SYNC_MY_SEQUENCE';
BEGIN
DBMS_LOCK.ALLOCATE_UNIQUE (C_LOCK_KEY,V_LOCK_HANDLE,10);
--Serialize access
V_LOCK_STATUS := DBMS_LOCK.REQUEST(
LOCKHANDLE => V_LOCK_HANDLE,
LOCKMODE => DBMS_LOCK.X_MODE,
TIMEOUT => 10,
RELEASE_ON_COMMIT => TRUE);
SELECT
LAST_NUMBER,
CACHE_SIZE
INTO V_LAST_NUMBER, V_CACHE_SIZE
FROM USER_SEQUENCES
WHERE SEQUENCE_NAME = 'MY_SEQUENCE';
IF P_CANDIDATE_MAX_VALUE >= (V_LAST_NUMBER - V_CACHE_SIZE)
THEN
V_SEQUENCE_DELTA := P_CANDIDATE_MAX_VALUE + V_CACHE_SIZE - V_LAST_NUMBER ;
V_SEQUENCE_GAP := GREATEST(V_SEQUENCE_DELTA, V_CACHE_SIZE);
EXECUTE IMMEDIATE 'ALTER SEQUENCE MY_SEQUENCE INCREMENT BY '||V_SEQUENCE_GAP;
V_NEXTVAL := MY_SEQUENCE.NEXTVAL;
EXECUTE IMMEDIATE 'ALTER SEQUENCE MY_SEQUENCE INCREMENT BY 1';
FOR CACHE_POINTER IN 1..V_CACHE_SIZE LOOP
V_NEXTVAL := MY_SEQUENCE.NEXTVAL;
END LOOP;
END IF;
COMMIT;
END;
/
Create the trigger:
CREATE OR REPLACE TRIGGER MAYBE_SET
BEFORE INSERT ON MY_TABLE
FOR EACH ROW
BEGIN
IF :NEW.MY_TABLE_ID IS NULL
THEN
:NEW.MY_TABLE_ID := MY_SEQUENCE.NEXTVAL;
ELSE
SYNC_MY_SEQUENCE(:NEW.MY_TABLE_ID);
END IF;
END;
/
Then test it:
INSERT INTO MY_TABLE SELECT LEVEL FROM DUAL CONNECT BY LEVEL < 5;
SELECT * FROM MY_TABLE ORDER BY 1 ASC;
MY_TABLE_ID
1
2
3
4
It just used the NEXTVAL each time.
Then add a collidable value. Adding this will fire the sproc and do the extra work to push the SEQUENCE into a safe zone.
INSERT INTO MY_TABLE VALUES(5);
SELECT * FROM MY_TABLE ORDER BY 1 ASC;
MY_TABLE_ID
1
2
3
4
5
Then use NULL again:
INSERT INTO MY_TABLE VALUES(NULL);
SELECT * FROM MY_TABLE ORDER BY 1 ASC;
MY_TABLE_ID
1
2
3
4
5
41
The SEQUENCE had a costly operation to get there, but has settled and didn't collide.
If other provided values are below the SEQUENCE visibility, they add freely and don't change the NEXTVAL:
INSERT INTO MY_TABLE VALUES(7);
INSERT INTO MY_TABLE VALUES(19);
INSERT INTO MY_TABLE VALUES(-9999);
INSERT INTO MY_TABLE VALUES(NULL);
SELECT * FROM MY_TABLE ORDER BY 1 ASC;
MY_TABLE_ID
-9999
1
2
3
4
5
7
19
41
42
If the gap is huge, it jumps way out there:
INSERT INTO MY_TABLE VALUES(50000);
INSERT INTO MY_TABLE VALUES(NULL);
SELECT * FROM MY_TABLE ORDER BY 1 ASC;
MY_TABLE_ID
-9999
1
2
3
4
5
7
19
41
42
50000
50022
This could be too costly for your use case, and I haven't tested in in a RAC, but wanted to throw out an idea that can avoid collisions.

Related

Is there any way we can emulate the way of using :old and :new of row triggers in statement triggers in PL/SQL

Good Day Buddies!
So, here is my Question, it says -
Write a update, delete trigger on clientmstr table. The System
should keep track of the records that ARE BEING updated or
deleted. The old value of updated or deleted records should be
added in audit_trade table. (Separate implementation using both row
and statement triggers)
And my solution looks like this -
-- For row trigger
create or replace trigger row_trigger
before delete or update on client_master
referencing old as old new as new
for each row
begin
insert into audit_table values(
:old.client_id, :old.client_name, :old.client_budget
);
end;
/
And as per the question I have to implement the same using statement trigger but I couldn't think of a way it can be done. I studied about statement triggers and I learned that we can't use :old and :new here. Is there any way we can implement the same row trigger method of adding in audit table using statement trigger? I am just starting out and it's just been two days I started learning PL/SQL. I spend whole day searching everywhere on the internet - tried looking for an example but I am not getting it. Can anyone help?
Edit
(1) I am using Oracle SQL Developer
(2) As someone suggested in comments - it isn't possible to do this in statement trigger, I think the same. I have to submit my assignment this Saturday. I had a conversation with my teacher - she said it's possible to implement it using statement trigger. I asked her how - but she didn't responded. Then I asked her for a hint and she said this (I'm copy pasting her text)-
Create a separate table with col as operations and timestamp. Write statement level trigger on insert update and delete operations. The trigger will capture the operation fired and timestamp by inserting values in table.
I am not getting what does that mean or how to do it! Can anyone help me solve this?
You could use a compound trigger.
Create the types:
CREATE TYPE client_master_obj IS OBJECT(
id NUMBER,
name VARCHAR2(20),
budget NUMBER(10,2)
);
CREATE TYPE client_master_table IS TABLE OF client_master_obj;
Then the trigger:
CREATE TRIGGER client_master_cmp_trigger
FOR DELETE OR UPDATE ON client_master
COMPOUND TRIGGER
data client_master_table := client_master_table();
AFTER EACH ROW
IS
BEGIN
data.EXTEND(1);
data(data.COUNT) := client_master_obj(
:OLD.client_id,
:OLD.client_name,
:OLD.client_budget
);
END AFTER EACH ROW;
AFTER STATEMENT
IS
BEGIN
INSERT INTO audit_table (client_id, client_name, client_budget, trg_type)
SELECT id,
name,
budget,
'C'
FROM TABLE(data);
END AFTER STATEMENT;
END;
/
Which, for the sample data:
CREATE TABLE client_master (client_id, client_name, client_budget) AS
SELECT 1, 'Alice', 100 FROM DUAL UNION ALL
SELECT 2, 'Beryl', 200 FROM DUAL UNION ALL
SELECT 3, 'Carol', 300 FROM DUAL UNION ALL
SELECT 4, 'Debra', 400 FROM DUAL UNION ALL
SELECT 5, 'Emily', 500 FROM DUAL;
CREATE TABLE audit_table (client_id, client_name, client_budget, trg_type) AS
SELECT cm.*, 'X' FROM client_master cm WHERE 1 = 0;
Then after:
UPDATE client_master
SET client_budget = client_budget + 600
WHERE client_id IN (1, 2);
DELETE FROM client_master WHERE client_id IN (1, 3);
Then the audit table contains (with the row trigger also firing for the same changes):
SELECT * FROM audit_table;
CLIENT_ID
CLIENT_NAME
CLIENT_BUDGET
TRG_TYPE
1
Alice
100
R
2
Beryl
200
R
1
Alice
100
C
2
Beryl
200
C
1
Alice
700
R
3
Carol
300
R
1
Alice
700
C
3
Carol
300
C
db<>fiddle here
Same approach using compound trigger, but although is not literally a level statement trigger, because normally they refer to table level triggers.
create or replace trigger row_compound_trigger
for delete or update on client_master
compound trigger
--
-- an array structure to buffer all the row changes
--
type t_row_list is
table of client_master%rowtype index by pls_integer;
l_audit_rows t_row_list;
l_operation varchar2(1) :=
case when updating then 'U'
when deleting then 'D'
end;
before statement is
begin
--
-- initialize the array
--
l_audit_rows.delete;
end before statement;
after each row is
begin
--
-- at row level, capture all the changes into the array
-- this variables use sys_context in case you want to use it ( not needed )
--
l_audit_rows(l_audit_rows.count+1).aud_who := sys_context('USERENV','SESSION_USER');
l_audit_rows(l_audit_rows.count).aud_when := sysdate;
l_audit_rows(l_audit_rows.count).aud_operation := l_operation;
l_audit_rows(l_audit_rows.count).aud_module := sys_context('USERENV','MODULE');
if updating then
l_audit_rows(l_audit_rows.count).client_id := :new.client_id
l_audit_rows(l_audit_rows.count).client_name := :new.client_name
... all the fields
else
l_audit_rows(l_audit_rows.count).client_id := :old.client_id
l_audit_rows(l_audit_rows.count).client_name := :old.client_name
... all the fields
end if;
end after each row;
after statement is
begin
--
-- then at completion, do a single insert of all the rows into our audit table
--
forall i in 1 .. l_audit_rows.count
insert into audit_table values l_audit_rows(i);
l_audit_rows.delete;
end after statement;
end;
/

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;

Why row is visible to several sessions when selected FOR UPDATE SKIP LOCKED?

Assume there are two tables TST_SAMPLE (10000 rows) and TST_SAMPLE_STATUS (empty).
I want to iterate over each record in TST_SAMPLE and add exactly one record to TST_SAMPLE_STATUS accordingly.
In a single thread that would be simply this:
begin
for r in (select * from TST_SAMPLE)
loop
insert into TST_SAMPLE_STATUS(rec_id, rec_status)
values (r.rec_id, 'TOUCHED');
end loop;
commit;
end;
/
In a multithreaded solution there's a situation, which is not clear to me.
So could you explain what causes processing one row of TST_SAMPLE several times.
Please, see details below.
create table TST_SAMPLE(
rec_id number(10) primary key
);
create table TST_SAMPLE_STATUS(
rec_id number(10),
rec_status varchar2(10),
session_id varchar2(100)
);
begin
insert into TST_SAMPLE(rec_id)
select LEVEL from dual connect by LEVEL <= 10000;
commit;
end;
/
CREATE OR REPLACE PROCEDURE tst_touch_recs(pi_limit int) is
v_last_iter_count int;
begin
loop
v_last_iter_count := 0;
--------------------------
for r in (select *
from TST_SAMPLE A
where rownum < pi_limit
and NOT EXISTS (select null
from TST_SAMPLE_STATUS B
where B.rec_id = A.rec_id)
FOR UPDATE SKIP LOCKED)
loop
insert into TST_SAMPLE_STATUS(rec_id, rec_status, session_id)
values (r.rec_id, 'TOUCHED', SYS_CONTEXT('USERENV', 'SID'));
v_last_iter_count := v_last_iter_count + 1;
end loop;
commit;
--------------------------
exit when v_last_iter_count = 0;
end loop;
end;
/
In the FOR-LOOP I try to iterate over rows that:
- has no status (NOT EXISTS clause)
- is not currently locked in another thread (FOR UPDATE SKIP LOCKED)
There's no requirement for the exact amount of rows in an iteration.
Here pi_limit is just a maximal size of one batch. The only thing needed is to process each row of TST_SAMPLE in exactly one session.
So let's run this procedure in 3 threads.
declare
v_job_id number;
begin
dbms_job.submit(v_job_id, 'begin tst_touch_recs(100); end;', sysdate);
dbms_job.submit(v_job_id, 'begin tst_touch_recs(100); end;', sysdate);
dbms_job.submit(v_job_id, 'begin tst_touch_recs(100); end;', sysdate);
commit;
end;
Unexpectedly, we see that some rows were processed in several sessions
select count(unique rec_id) AS unique_count,
count(rec_id) AS total_count
from TST_SAMPLE_STATUS;
| unique_count | total_count |
------------------------------
| 10000 | 17397 |
------------------------------
-- run to see duplicates
select *
from TST_SAMPLE_STATUS
where REC_ID in (
select REC_ID
from TST_SAMPLE_STATUS
group by REC_ID
having count(*) > 1
)
order by REC_ID;
Please, help to recognize mistakes in implementation of procedure tst_touch_recs.
Here's a little example that shows why you're reading rows twice.
Run the following code in two sessions, starting the second a few seconds after the first:
declare
cursor c is
select a.*
from TST_SAMPLE A
where rownum < 10
and NOT EXISTS (select null
from TST_SAMPLE_STATUS B
where B.rec_id = A.rec_id)
FOR UPDATE SKIP LOCKED;
type rec is table of c%rowtype index by pls_integer;
rws rec;
begin
open c; -- data are read consistent to this time
dbms_lock.sleep ( 10 );
fetch c
bulk collect
into rws;
for i in 1 .. rws.count loop
dbms_output.put_line ( rws(i).rec_id );
end loop;
commit;
end;
/
You should see both sessions display the same rows.
Why?
Because Oracle Database has statement-level consistency, the result set for both is frozen when you open the cursor.
But when you have SKIP LOCKED, the FOR UPDATE locking only kicks in when you fetch the rows.
So session 1 starts and finds the first 9 rows not in TST_SAMPLE_STATUS. It then waits 10 seconds.
Provided you start session 2 within these 10 seconds, the cursor will look for the same nine rows.
At this point no rows are locked.
Now, here's where it gets interesting.
The sleep in the first session will finish. It'll then fetch the rows, locking them and skipping any that are already locked.
Very shortly after, it'll commit. Releasing the lock.
A few moments later, session 2 comes to read these rows. At this point the rows are not locked!
So there's nothing to skip.
How exactly you solve this depends on what you're trying to do.
Assuming you can't move to a set-based approach, you could make the transactions serializable by adding:
set transaction isolation level serializable;
before the cursor loop. This will then move to transaction-level consistency. Enabling the database to detect "something changed" when fetching rows.
But you'll need to catch ORA-08177: can't serialize access for this transaction errors in your within the outer loop. Or any process that re-reads the same rows will drop out at this point.
Or, as commenters have suggested used Advanced Queueing.

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;
/

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')