I have a students table in my Oracle database which has a field called RECORD_NUMBER. This field is 8 characters long and I want to create a trigger to pad the left part out with 0s as it's inserted. This is what I have so far:
create or replace
TRIGGER STUDENTS_RECORD_NUMBER_TRG
BEFORE INSERT OR UPDATE OF RECORD_NUMBER ON TBL_STUDENTS
FOR EACH ROW
BEGIN
WHILE length(:new.RECORD_NUMBER) < 9
LOOP
:new.RECORD_NUMBER := LPAD(:new.RECORD_NUMBER,8,'0');
END LOOP;
NULL;
END;
However, when I try to insert a row the database connection locks up and I have to restart Oracle to use it again. Is it possible that this trigger is causing an infinite loop?
If record_number is a varchar2(8), then length(:new.record_number) will always be less than 9 and your loop will iterate endlessly. But you don't need a loop here, just call LPAD
create or replace TRIGGER STUDENTS_RECORD_NUMBER_TRG
BEFORE INSERT OR UPDATE OF RECORD_NUMBER
ON TBL_STUDENTS
FOR EACH ROW
BEGIN
:new.RECORD_NUMBER := LPAD(:new.RECORD_NUMBER,8,'0');
END;
This assumes, of course, that it really makes sense to pad the data that is physically stored in the database rather than doing something like applying the LPAD in a view layer. Generally, I would expect that you'd be better served putting this sort of presentation logic in a view since views are great for implementing presentation logic. But this trigger should do what you've asked.
Leave the entry as a number. If it needs to be displayed with padded zeroes then do so as part of the display logic.
Related
I get an error (ORA-04091: table DBPROJEKT_AKTIENDEPOT.AKTIE is mutating, trigger/function may not see it) when executing my trigger:
CREATE OR REPLACE TRIGGER Aktien_Bilanz_Berechnung
AFTER
INSERT OR UPDATE OF TAGESKURS
OR INSERT OR UPDATE OF WERT_BEIM_EINKAUF
ON AKTIE
FOR EACH ROW
DECLARE
bfr number;
Begin
bfr := :new.TAGESKURS - :new.WERT_BEIM_EINKAUF;
UPDATE AKTIE
SET BILANZ = TAGESKURS - WERT_BEIM_EINKAUF;
IF bfr < -50
THEN
DBMS_OUTPUT.PUT_LINE('ACHTUNG: The value (Nr: '||:new.AKTIEN_NR||') is very low!');
END IF;
END;
I want to check the value "BILANZ" after calculating it, wether it is under -50.
Do you have any idea why this error is thrown?
Thanks for any help!
There are several issues here:
Oracle does not allow you to perform a SELECT/INSERT/UPDATE/DELETE against a table within a row trigger defined on that table or any code called from such a trigger, which is why an error occurred at run time. There are ways to work around this - for example, you can read my answers to this question and this question - but in general you will have to avoid accessing the table on which a row trigger is defined from within the trigger.
The calculation which is being performed in this trigger is what is referred to as business logic and should not be performed in a trigger. Putting logic such as this in a trigger, no matter how convenient it may seem to be, will end up being very confusing to anyone who has to maintain this code because the value of BILANZ is changed where someone who is reading the application code's INSERT or UPDATE statement can't see it. This calculation should be performed in the INSERT or UPDATE statement, not in a trigger. It considered good practice to define a procedure to perform INSERT/UPDATE/DELETE operations on a table so that all such calculations can be captured in one place, instead of being spread out throughout your code base.
Within a BEFORE ROW trigger you can modify the values of the fields in the :NEW row variable to change values before they're written to the database. There are times that this is acceptable, such as when setting columns which track when and by whom a row was last changed, but in general it's considered a bad idea.
Best of luck.
You are modifying the table with the trigger. Use a before update trigger:
CREATE OR REPLACE TRIGGER Aktien_Bilanz_Berechnung
BEFORE INSERT OR UPDATE OF TAGESKURS OR INSERT OR UPDATE OF WERT_BEIM_EINKAUF
ON AKTIE
FOR EACH ROW
DECLARE
v_bfr number;
BEGIN
v_bfr := :new.TAGESKURS - :new.WERT_BEIM_EINKAUF;
:new.BILANZ := v_bfr;
IF v_bfr < -50 THEN
Raise_Application_Error(-20456,'ACHTUNG: The value (Nr: '|| :new.AKTIEN_NR || ') is very low!');
END IF;
END;
What's the difference between these two blocks and when to use the first or the second?
Create OR Replace trigger trig_before_insert before insert on Employee For each Row
Begin
DBMS_OUTPUT.PUT_LINE('Inserting');
END;
And
Create OR Replace trigger trig_before_insert before insert on Employee
Begin
DBMS_OUTPUT.PUT_LINE('Inserting');
END;
If you perform an
INSERT INTO EMPLOYEE
SELECT ...
and that SELECT returns 100 rows so that the INSERT inserts 100 rows, your first trigger will execute 100 times, once for each row. In the same situation, your second trigger will execute only once.
You can use a BEFORE INSERT...FOR EACH ROW trigger to change the values that are being inserted by accessing them via the :NEW variable. E.g.,
:new.column_1 := 'a different value';
You cannot do that in a statement level trigger (which is what your 2nd trigger is).
There are also limitations in row level triggers (which is what your 1st trigger is). In particular, you may not SELECT from the trigger's base table (EMPLOYEES in this case), because that table is said to be "mutating". The exact reasons, as I understand them, go back to the core principles of relational databases -- specifically that the results of a statement (like INSERT INTO...SELECT) should not depend on the order in which the rows are processed. There are workarounds to this limitation, however, which are beyond the scope of your original question, I think.
Closed. This question is off-topic. It is not currently accepting answers.
Want to improve this question? Update the question so it's on-topic for Stack Overflow.
Closed 10 years ago.
Improve this question
I have finished my first real PL/SQL stored proc, this stored proc works as expected. I am new to PL/SQL, could you please point anything wrong or bad coding ?
This code is assuming a naming convention, for example, 't_company' table will use 'companyId' as its primary key and its type is number.
Thank you very much.
create or replace
package body test_erp AS
procedure init_data is
begin
logMessage('procedure init_data');
SAVEPOINT do_insert;
insert into t_company(companyId, companyName) values(gen_key('t_company'), 'IBM');
COMMIT;
exception
WHEN OTHERS THEN
rollback to do_insert;
logMessage('roll back , due to '|| SQLERRM);
end init_data;
end test_erp;
It will call this function
create or replace
function gen_key(tblName varchar2)
return number is
l_key number := 1000;
l_tmpStr varchar(2000); -- not good, how to fix it ?
begin
l_tmpStr := substr(tblName, 3, length(tblName));
EXECUTE IMMEDIATE ' SELECT CASE WHEN MAX('||l_tmpStr||'Id) IS NULL THEN 1000 ELSE MAX('||l_tmpStr||'Id)+1 END FROM '|| tblName into l_key;
logmessage('gen primary key '|| tblName ||' '||l_key);
return l_key;
end;
Your key_gen procedure is rather problematic. Generating keys by doing a MAX(key)+1 is slow and will not work in a multiuser environment. Assuming you have two users, it is relatively easy for both users to see the same MAX(key) and try to insert rows with the same primary key.
Oracle provides sequences in order to efficiently generate primary keys in a multi-user environment. You would be much better served using sequences to generate your keys. Conventionally, you would create one sequence per table, i.e.
CREATE SEQUENCE company_seq;
Your INSERT statement would then be something like
insert into t_company(companyId, companyName) values(company_seq.nextval, 'IBM');
Or you could create a trigger on the table to automatically populate the primary key.
Additionally, while it is fine to catch exceptions in order to log them, you really want to re-raise that exception so that the caller is aware that the INSERT failed.
Using function in your case gen_key is very slow and it's incorrect database-written and also very inefficiently.
So my advice is to create SEQUENCE that is generally used for this.Then you should create TRIGGER for generating new PK for each INSERT or directly add it with NEXTVAL.
So, your SEQUENCE can looks like this:
CREATE SEQUENCE YOUR_COMP_SEQ
MINVALUE 1
MAXVALUE 999999
START WITH 1
INCREMENT BY 1
NOCACHE
;
Then i recommend to you use meant TRIGGER:
CREATE OR REPLACE TRIGGER AUTOSET_ID_COMP
BEFORE INSERT ON t_company
FOR EACH ROW
BEGIN
SELECT YOUR_COMP_SEQ.NEXTVAL INTO :NEW.companyId FROM DUAL;
END;
And finally just call query:
INSERT INTO t_company(companyName) VALUES('SomeValue');
If you don't want to create TRIGGER so you can do it directly like this:
INSERT INTO t_company(companyId, companyName)
VALUES(YOUR_COMP_SEQ.NEXTVAL, 'SomeValue');
Note: Of course, you can create for every TABLE its own SEQUENCE and then use TRIGGERS for each TABLE.
Note 2: Sequences are very good but there is some problem that for example you added to table 20 rows, so IDs are 1,2,3, ... etc. and for example you will delete 15. row and since this ID 15 you can't use, anymore.
Update:
Answer and Solution is updated after a little discussion with #Ben, thanks.
I'm having requirement to generate a insert script file from excel sheet. I'm partly successfully in generating script file. But i got struck in a situation,I need help from any1.
My logic is some thing like this, Read first cell,check if the value in the cell already exists in DB.If not, generate an insert script as follow
declare
PK1 integer;
begin
select tablename_seq.currval into PK1 from dual;
insert into TableName valuestablename_seq_seq.nextval,'Blagh',1);
end;
Im storing PK1 in hashtable with data has KEY .so that if the same data appears in the next rows,using Hashtable search, I will get the hashtable value for corresponding data key and pass it has parameter to another insert script. But every time i generate new variable like PK1,Pk2...etc.I have keep 'BEGIN' key word after Declare and also add 'END' key word after every insert,If i do so scope of variable goes out off scope.I may be using those declared variables in another insert statements has a parameter. Is there any chance of saving PK1,Pk2..... has session/Global variables for the script execution. So they wil become avialable for entire script execution time.
My inclination is to say that each line of your spreadsheet should just be creating a statement like insert into TableName values (tablename_seq_seq.nextval,'Blagh',1) returning ID into PK1;, then wrap the whole thing in a single DECLARE-BEGIN-END block with the appropriate variables defined, something like:
declare
pk1 integer;
pk2 integer;
begin
insert into TableName
values (tablename_seq_seq.nextval,'Blagh',1)
returning ID into PK1;
insert into TableName
values (tablename_seq_seq.nextval,'Urgh',2)
returning ID into PK2;
[...]
end;
You could even create the list of variable declarations in one column and the SQL in another, then copy and paste them into the right place in the block.
I'd start off with a
DECLARE
PROCEDURE action (p_val IN INTEGER) IS
...
END action;
BEGIN
Then have each line in the spreadsheet just do a call to the procedure so that a spreadsheet entry of 1 becomes
action (1);
Then you end up with something like
DECLARE
PROCEDURE action (p_val IN INTEGER) IS
...
END action;
BEGIN
action (1);
action (8);
action (23);
action (1);
action (1);
END;
The action procedure can be as complicated as you like, storing information in tables/arrays whatever.
Is the question about the best way to update a database from a spreadsheet, or the best way to generate a script from a spreadsheet?
I would recommend loading the spreadsheet data into a temporary table and then using a simple INSERT/SELECT statement, unless you're worried about uniqueness collisions in which case I would use a MERGE statement instead. This is much easier than trying to generate a script with logic for each insert statement.
Instead of each Line, Consider as each cell. Each cell will generate insert script into corresponding cell. I have created in the same way, problem is If i want to use PK1 variable some where in the row 10 ,column 10 (cell value) insert script,because we are ending 'end' immediately after Begin block, the scope of PK1 always be remain in Begin block.For this i have created Begin with one insert and then create another Begin with another insert and so on.and # the end im adding end;end;But the problem with above method is,I'm trrying to insert 200 row X 200 columns = 400 cells insert scripts. in this flow when i try to run script, it throws an runtime error ' Stack Overflow'
If you use Oracle E-Business, you might be interested by webadi.
This tool creates a excel files to be populated and then loaded into database via a procedure. You can then validate your data.
Creating custom Web ADI Integrators
WebADI - Using a Custom Integrator
Is it possible to dynamically reference the :NEW/OLD pseudo records, or copy them?
I'm doing a audit trigger for a very wide table, so would like to avoid having separate triggers for insert/delete/update.
When updating/inserting I want to record the :NEW values in the audit table, when deleting I want to record the :OLD values.
create or replace trigger audit_tgr
before insert or update or delete on 'table_name'
for each row
begin
if (inserting or updating) then
insert into audit table (a,b,c) values(:new.a,:new.b,:new.c);
else
insert into audit table (a,b,c) values(:old.a,:old.b,:old.c);
end;
You could try:
declare
l_deleting_ind varchar2(1) := case when DELETING then 'Y' end;
begin
insert into audit_table (col1, col2)
values
( CASE WHEN l_deleting_ind = 'Y' THEN :OLD.col1 ELSE :NEW.col1 END
, CASE WHEN l_deleting_ind = 'Y' THEN :OLD.col2 ELSE :NEW.col2 END
);
end;
I found that the variable was required - you can't access DELETING directly in the insert statement.
WOW, You want to have only ONE insert in your trigger to avoid what?
"I have a single insert statement INSERT INTO HIST ( EMP_ID, NAME ) VALUES (:NEW.EMP_ID , :NEW.NAME ) ; when deleting though, I want to use :OLD , not not have a seperate insert statement for that. "
It's a wide table. SO? It's not like there no REPLACE in text editors, you're not going to write the Insert again, just copy, paste, select, replace :NEW with :OLD.
Tony does have a solution but I seriously doubt that performs better than 2 inserts would perform.
What's the big deal?
EDIT
the main thing I'm trying to avoid is having to managed 2 inserts when the table changes. – Matthew Watson
I battle this attitude all the time. Those who write Java or C++ or .Net have a built-in RBO... Do this, this is good. Don't do that, that's bad. They write code according to these rules and that's fine. The problem is when these rules are applied to databases. Databases don't behave the same way code does.
In the code world, having essentially the same code in two "places" is bad. We avoid it. One would abstract that code to a function and call it from the two places and thus avoid maintaining it twice, and possibly missing one, etc. We all know the drill.
In this case, while it's true that in the end I recommend two inserts, they are separated by an ELSE. You won't change one and forget the other one. IT'S Right There. It's not in a different package, or in some compiled code, or even somewhere else in the same trigger. They're right beside each other, there's an ELSE and the Insert is repeated with :NEW, instead of :OLD. Why am I so crazed about this? Does it really make a difference here? I know two inserts won't be worse than other ideas, and it could be better.
The real reason is being prepared for the times when it does matter. If you're avoiding two inserts just for the sake of maintenance, you're going to miss the times when this makes a HUGE difference.
INSERT INTO log
SELECT * FROM myTable
WHERE flag = 'TRUE'
ELSE -- column omitted for clarity
INSERT INTO log
SELECT * FROM myTable
WHERE flag = 'FALSE'
Some, including Matthew, would say this is bad code, there are two inserts. I could easily replace 'TRUE' and 'FALSE' with a bind variable and flip it at will. And that's what most people would do. But if True is .1% of the values and 99.9% is False, you want two inserts, because you want two execution plans. One is better off with an index and the other an FTS. So, yes, you do have two Inserts to maintain. That's not always bad and in this case it's good and desirable.
You can use a compound trigger and programmatically check if it us I/U/D.
Compound Triggers
Why don't you use Oracle's built in standard or fine-grained auditing?
Use a compound trigger, as others have suggested. Save the old or new values, as appropriate, to variables, and use the variables in your insert statement:
declare
v_col1 table_name.col1%type;
v_col2 table_name.col2%type;
begin
if deleting then
v_col1 := :old.col1;
v_col2 := :old.col2;
else
v_col1 := :new.col1;
v_col2 := :new.col2;
end if;
insert into audit_table(col1, col2)
values(v_col1, v_col2);
end;