I have never created a trigger in Oracle before so I am looking for some direction.
I would like to create a trigger that increments an ID by one if the ID isnt in the insert statement.
The ID should start at 10000, and when a record is inserted the next ID should be 10001. If the insert statement contains a ID, it should override the auto increment.
ie
insert into t1 (firstname, lastname) values ('Michael','Jordan'),('Larry','Bird')
should look like:
firstname lastname id
Micahel Jordan 10000
Larry Bird 10001
insert into t1 (firstname, lastname, id) values ('Scottie','Pippen',50000)
should look like:
firstname lastname id
Micahel Jordan 10000
Larry Bird 10001
Scottie Pippen 50000
Something like this will work on 11g
CREATE SEQUENCE t1_id_seq
start with 10000
increment by 1;
CREATE TRIGGER trigger_name
BEFORE INSERT ON t1
FOR EACH ROW
DECLARE
BEGIN
IF( :new.id IS NULL )
THEN
:new.id := t1_id_seq.nextval;
END IF;
END;
If you're on an earlier version, you'll need to do a SELECT INTO to get the next value from the sequence
CREATE TRIGGER trigger_name
BEFORE INSERT ON t1
FOR EACH ROW
DECLARE
BEGIN
IF( :new.id IS NULL )
THEN
SELECT t1_id_seq.nextval
INTO :new.id
FROM dual;
END IF;
END;
Be aware that Oracle sequences are not gap-free. So it is entirely possible that particular values will be skipped for a variety of reasons. Your first insert may have an ID of 10000 and the second may have an ID of 10020 if it's done minutes, hours, or days later.
Additionally, be aware that Oracle does not support specifying multiple rows in the VALUES clause as MySQL does. So rather than
insert into t1 (firstname, lastname) values ('Michael','Jordan'),('Larry','Bird')
you'd need two separate INSERT statements
insert into t1 (firstname, lastname) values ('Michael','Jordan');
insert into t1 (firstname, lastname) values ('Larry','Bird');
I would recommend to code this trigger with a condition on the trigger itself, not in the sql block.
CREATE OR REPLACE TRIGGER your_trigger
BEFORE INSERT ON your_table
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
WHEN (new.id IS NULL)
BEGIN
SELECT your_sequence.nextval
INTO :new.id
FROM dual;
END;
/
With this solution the trigger is only executed if the condition matches (id is null).
Otherwise the trigger is always executed and the block checks if id is null. The DB must execute the SQL block which does nothing on not null values.
Related
I have been given an assignment to create a trigger that works when tables are inserted or updated or deleted in a table. If it is a delete or update then the table must store the older values before the action on another table. If it is insert then the new row should be added to the new table. Also it should include the number of rows that are affected by each action. So far this is what I have done:
CREATE OR REPLACE TRIGGER archive_update
BEFORE INSERT OR UPDATE ON EMPLOYEE
FOR EACH ROW
BEGIN
INSERT INTO archive_emp(EMP_ID, FIRST_NAME, LAST_NAME, BIRTH_DAY,
SEX, SALARY, SUPER_ID, BRANCH_ID)
VALUES(:new.EMP_ID, :new.FIRST_NAME, :new.LAST_NAME,
:new.BIRTH_DAY, :new.SEX, :new.SALARY, :new.SUPER_ID, :new.BRANCH_ID);
END;
To check the dml operation you may use an IF condition with appropriate dml predicate
CREATE OR REPLACE TRIGGER archive_update
BEFORE
INSERT OR UPDATE ON EMPLOYEE
FOR EACH ROW
BEGIN
IF UPDATING OR DELETING THEN --You may add this
INSERT INTO archive_emp( emp_id, first_name, last_name,
birth_day, sex,salary,super_id,branch_id
) VALUES (:old.emp_id, :old.first_name,:old.last_name,:old.birth_day,
:old.sex,:old.salary,:old.super_id,:old.branch_id
);
ELSIF INSERTING THEN --and this
INSERT INTO archive_emp( emp_id, first_name, last_name,
birth_day, sex,salary,super_id,branch_id
) VALUES (:new.emp_id, :new.first_name,:new.last_name,:new.birth_day,
:new.sex,:new.salary,:new.super_id,:new.branch_id
);
END IF;
END;
/
I'm creating a update trigger where an employee can never have a salary that is greater than the president's. However I need to subquery the president's salary for comparison and the "new" updated employee's salary.
I originally had the the subquery using from the from employees table but had to make a new table because of the mutating table problem. I don't think creating a new table is plausible solution for a real implementation.
Is there a way I can save the president's salary without creating a new table?
CREATE OR REPLACE TRIGGER prevent_salary
BEFORE UPDATE ON employees
FOR EACH ROW
declare pres_sal number(8,2);
BEGIN
select salary into pres_sal from employees_salary where job_id='AD_PRES';--employees_salary was employees but that gives mutating error
IF (:new.salary > pres_sal)
THEN UPDATE employees
SET salary = :old.salary
WHERE employee_id = :old.employee_id;
END IF;
END;
One way to do it is to save off the president's salary in a BEFORE STATEMENT trigger and then use that in the FOR EACH ROW trigger.
"Compound Triggers", which have been around at least since version 11.1, offer a nice way to do that all in one place.
Here is an example:
CREATE OR REPLACE TRIGGER prevent_salary
FOR UPDATE OF salary ON employees
COMPOUND TRIGGER
pres_sal NUMBER;
BEFORE STATEMENT IS
BEGIN
select salary
into pres_sal
from employees
where job_id='AD_PRES';
END BEFORE STATEMENT;
BEFORE EACH ROW IS
BEGIN
:new.salary := least(:new.salary, pres_sal);
END BEFORE EACH ROW;
END prevent_salary;
You may try this :
CREATE OR REPLACE TRIGGER prevent_salary
BEFORE UPDATE ON employees
FOR EACH ROW
declare pres_sal number(8,2);
BEGIN
select salary into pres_sal from employees_salary where job_id='AD_PRES';--employees_salary was employees but that gives mutating error
IF (:new.salary > pres_sal)
Raise_Application_Error (-20101, 'An employee''s salary couldn''t exceed president''s !');
END IF;
END;
I have a table in Oracle 12c which implements a sequence to increment ID's on inserts:
CREATE SEQUENCE
"ORCL_WD"."BIR_GRIDBASE_ID_SEQ"
MINVALUE 1 MAXVALUE 999999999999999999999999
INCREMENT BY 1 START WITH 61 CACHE 20 NOORDER NOCYCLE NOPARTITION ;
and the trigger for this:
create or replace trigger BIR_GRIDBASE_TRIG
before insert on "ORCL_WD"."BIR_GRIDBASE"
for each row
begin
if inserting then
if :NEW."ID" is null then
select BIR_GRIDBASE_ID_SEQ.nextval into :NEW."ID" from dual;
end if;
end if;
end;
This sequence works, but I have some gaps in my ID fields, so when inserting new items into this table, the insert duplicates ID's.
How do I prevent this?
Check your last ID value in table
Check your status SEQUENCE
SELECT COUNT(*)
FROM user_sequences
WHERE sequence_name = 'BIR_GRIDBASE_ID_SEQ';
Maybe you add value to this table without sequnce and now you have duplicate
You include the line:
if :NEW."ID" is null then
This means that if the ID the user has tried to insert is not NULL then it will just continue using the user-proposed ID value and not the sequence value.
The simplest way to solve this is to not use a trigger and just include the sequence value in the insert:
INSERT INTO ORCL_WD.BIR_GRIDBASE (
ID,
COLUMN_A,
COLUMN_B
) VALUES (
ORCL_WD.BIR_GRIDBASE_ID_SEQ,
'Value A',
'Value B'
);
Then the user cannot propose id values and the id value will always be a distinct value.
If you are reliant on using the trigger then you should take a long look at whether it is necessary to allow the user to propose ID values and, if so, you will have to code defensively to prevent these duplicates occurring (i.e. skip some sequence values). If it isn't then you could just remove the if :NEW."ID" is null then statement like this:
create or replace trigger BIR_GRIDBASE_TRIG
before insert on "ORCL_WD"."BIR_GRIDBASE"
for each row
begin
:NEW.ID := BIR_GRIDBASE_ID_SEQ.nextval;
end;
/
and all the ID values will come from the sequence.
Is it possible to reference a second table within a trigger?
create or replace trigger table1
before update of status_code on table1
for each row
declare z_user_id table2.user_id;
begin
if :new.status_code in (30,40) then
:new.z_open_01 := nvl(:OLD.z_user_id, nvl(:NEW.z_user_id, :old.z_open_01));
end if;
end;
/
As to your question: Yes.
The :old and :new constructs only apply to the table that cause the trigger to fire.
The way you have that written you are triggering off of Table1, but your description sounds like you want to grab a value from table one when something in Table2 changes.
But you didn't say what you want to DO with the userid you grab.
If you create a trigger on Table2, read a value from Table1, and save the value in a column in the row that fired the trigger you are gtg. Other scenarios get more complex.
so, based on comments something like:
create or replace trigger table2
after update of status_code on table2
for each row when (:new.status_code in (30,40))
declare t1_user Table1.user_id%type;
begin
Select user_id into t1_user from Table1 where <condition>
:new.z_open_01 := t1_user;
end if;
end;
/
setting up whatever condition select the desired user from Table1.
I have some problem executing the trigger below:
CREATE OR REPLACE TRIGGER AFTERINSERTCREATEBILL
AFTER INSERT
ON READING
FOR EACH ROW
DECLARE
varReadNo Int;
varMeterID Int;
varCustID Varchar(10);
BEGIN
SELECT SeqReadNo.CurrVal INTO varReadNo FROM DUAL;
Select MeterID INTO varMeterID
From Reading
Where ReadNo = varReadNo;
Select CustID INTO varCustID
From Address A
Join Meter M
on A.postCode = M.postCode
Where M.MeterID = varMeterID;
INSERT INTO BILL VALUES
(SEQBILLNO.NEXTVAL, SYSDATE, 'UNPAID' , 100 , varCustID , SEQREADNO.CURRVAL);
END;
Error Message:
*Cause: A trigger (or a user defined plsql function that is referenced in
this statement) attempted to look at (or modify) a table that was
in the middle of being modified by the statement which fired it.
*Action: Rewrite the trigger (or function) so it does not read that table.
Does it mean that I am not suppose to retrieve any details from table Reading?
I believe that the issue occur as I retrieving data from Table Reading while it is inserting the value:
SELECT SeqReadNo.CurrVal INTO varReadNo FROM DUAL;
Select MeterID INTO varMeterID
From Reading
Where ReadNo = varReadNo;
It's there anyway to resolve this? Or is there a better method to do this? I need to insert a new row into table BILL after entering the table Reading where the ReadNo need to reference to the ReadNo I just insert.
You cannot retrieve records from the same table in a row trigger. You can access values from actual record using :new and :old (is this your case?). The trigger could then be rewritten to
CREATE OR REPLACE TRIGGER AFTERINSERTCREATEBILL
AFTER INSERT
ON READING
FOR EACH ROW
DECLARE
varCustID Varchar(10);
BEGIN
Select CustID INTO varCustID
From Address A
Join Meter M
on A.postCode = M.postCode
Where M.MeterID = :new.MeterID;
INSERT INTO BILL VALUES
(SEQBILLNO.NEXTVAL, SYSDATE, 'UNPAID' , 100 , varCustID , SEQREADNO.CURRVAL);
END;
If you need to query other record from READING table you have to use a combination of statement triggers, row trigger and a PLSQL collection. Good example of this is on AskTom.oracle.com
Make sure that you have the necessary permissions on all the tables and access to the sequences you're using in the insert.
I haven't done Oracle in awhile, but you can also try querying dba_errors (or all_errors) in order to try and get more information on why your SP isn't compiling.
Something to the tune of:
SELECT * FROM dba_errors WHERE owner = 'THEOWNER_OF_YOUR_SP';
Add exception handling in your trigger and see what is happening, by doing it would be easy for you to track the exceptions.
CREATE OR REPLACE TRIGGER AFTERINSERTCREATEBILL
AFTER INSERT
ON READING
FOR EACH ROW
DECLARE
varCustID Varchar(10);
BEGIN
-- your code
EXCEPTION
WHEN NO_DATA_FOUND
THEN
DBMS_OUTPUT.PUT_LINE(TO_CHAR(SQLERRM(-20299)));
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(TO_CHAR(SQLERRM(-20298)));
END;
we can combined insert and select statement
CREATE OR REPLACE TRIGGER AFTERINSERTCREATEBILL
AFTER INSERT
ON READING
FOR EACH ROW
DECLARE
varCustID Varchar(10);
BEGIN
insert into bill
values
select SEQBILLNO.NEXTVAL,
SYSDATE,
'UNPAID' ,
100 ,
CustID,SEQREADNO.CURRVAL
From Address A
Join Meter M
on A.postCode = M.postCode
Where M.MeterID = :new.MeterID;
END;
try the above code.