I have a Users table, each User has a identifier, a name and some other fields.
I need to check if there is another user with the same name after inserting or updating, so I made this trigger:
CREATE OR REPLACE TRIGGER user_name
BEFORE
INSERT OR UPDATE
ON Users
FOR EACH ROW
DECLARE
count INTEGER;
BEGIN
SELECT COUNT(*) INTO count FROM Users WHERE name = :new.name AND idUser <> :new.idUser;
IF count > 0
THEN raise_application_error(-20007, 'There is already another user with the same name');
END IF;
END;
It seems to work when inserting new users, but it doesn't when I update them, it looks like it "ignores" the idUser check so it always fails as it finds the same user with the same name.
Why is this happening? Thank you
In triggers , there are 3 states as you know inserts update and delete , In update states there are new values and old values , I mean when you update a column you , you will replace the old value with new one , in trigger you can use the old and the new value of the column .. Please check this example
CREATE or REPLACE TRIGGER test001
AFTER INSERT OR UPDATE OR DELETE ON tabletest001
DECLARE
Operation NUMBER;
CustomerCode CHAR(10 BYTE);
BEGIN
IF INSERTING THEN
Operation := 1;
CustomerCode := :new.field1;
END IF;
IF UPDATING THEN
Operation := 2;
CustomerCode := :old.field1;
END IF;
// DO SOMETHING ...
EXCEPTION
WHEN OTHERS THEN ErrorCode := SQLCODE;
END;
/
in the above example , in update state I used old.value of the column , so in your example you should check the old value not new , please try it
if updating then
SELECT COUNT(*) INTO count FROM Users WHERE name = :old.name AND idUser <> :old.idUser;
IF count > 0
THEN raise_application_error(-20007, 'There is already another user with the same name');
end if
however as in the comments adviced you is to use primary key, however my answer is to give you understanding to triggers
Related
As discussed here, I'm unable to use :OLD and :NEW on columns with collation other than USING_NLS_COMP. I'm trying to find a way around this but haven't been successful so far.
This is the original trigger:
CREATE OR REPLACE TRIGGER SYS$PERSONSSALUTATIONAU
AFTER UPDATE ON PERSONS
FOR EACH ROW
begin
State_00.Salutations_ToDelete(State_00.Salutations_ToDelete.Count + 1) := :old.SalutationTitle;
State_00.Salutations_ToInsert(State_00.Salutations_ToInsert.Count + 1) := :new.SalutationTitle;
end;
This is what I've tried:
CREATE OR REPLACE TRIGGER SYS$PERSONSSALUTATIONAU
FOR UPDATE ON Persons
COMPOUND TRIGGER
TYPE Persons_Record IS RECORD (
SalutationTitle NVARCHAR2(30)
);
TYPE Persons_Table IS TABLE OF Persons_Record INDEX BY PLS_INTEGER;
gOLD Persons_Table;
gNEW Persons_Table;
BEFORE EACH ROW IS BEGIN
SELECT SalutationTitle
BULK COLLECT INTO gOLD
FROM Persons
WHERE ID = :OLD.ID;
END BEFORE EACH ROW;
AFTER EACH ROW IS BEGIN
SELECT SalutationTitle
BULK COLLECT INTO gNEW
FROM Persons
WHERE ID = :NEW.ID;
END AFTER EACH ROW;
AFTER STATEMENT IS BEGIN
FOR i IN 1 .. gNEW.COUNT LOOP
State_00.Salutations_ToDelete(State_00.Salutations_ToDelete.Count + 1) := gOLD(i).SalutationTitle;
State_00.Salutations_ToInsert(State_00.Salutations_ToInsert.Count + 1) := gNEW(i).SalutationTitle;
END LOOP;
END AFTER STATEMENT;
END;
This results in error ORA-04091. I've also tried moving the select into the AFTER STATEMENT section which works, but there is no way to access the old values. If somebody has a solution for this it would be most appreciated.
EDIT:
I created a minimal reproducible example:
CREATE TABLE example_table (
id VARCHAR2(10),
name NVARCHAR2(100)
);
CREATE TABLE log_table (
id VARCHAR2(10),
new_name NVARCHAR2(100),
old_name NVARCHAR2(100)
);
CREATE OR REPLACE TRIGGER example_trigger
AFTER UPDATE ON example_table
FOR EACH ROW BEGIN
INSERT INTO log_table VALUES(:old.id, :new.name, :old.name);
END;
INSERT INTO example_table VALUES('01', 'Daniel');
-- this works as expected
UPDATE example_table SET name = ' John' WHERE id = '01';
SELECT * FROM log_table;
DROP TABLE example_table;
CREATE TABLE example_table (
id VARCHAR2(10),
-- this is the problematic part
name NVARCHAR2(100) COLLATE XCZECH_PUNCTUATION_CI
);
INSERT INTO example_table VALUES('01', 'Daniel');
-- here nothing is inserted into log_example, if you try to
-- recompile the trigger you'll get error PLS-00049
UPDATE example_table SET name = ' John' WHERE id = '01';
SELECT * FROM log_table;
DROP TABLE example_table;
DROP TABLE log_table;
DROP TRIGGER example_trigger;
In the discussion you reference a document concerning USING_NLS_COMP. That has nothing to do with the error you are getting. The error ORA-04091 is a reference to the table that fired the trigger (mutating). More to come on this. I am not saying you do not have USING_NLS_COMP issues, just that they are NOT causing the current error.
There are misconceptions shown in your trigger. Beginning with the name itself; you should avoid the prefix SYS. This prefix is used by Oracle for internal objects. While SYS prefix is not specifically prohibited at best it causes confusion. If this is actually created in the SYS schema then that in itself is a problem. Never use SYS schema for anything.
There is no reason to create a record type containing a single variable, then create a collection of that type, and finally define variables of the collection. Just create a collection to the variable directly, and define variables of the collection.
The bulk collect in the select statements is apparently misunderstood as used. I assume you want to collect all the new and old values in the collections. Bulk collect however will not do this. Each time bulk collect runs the collection used is cleared and repopulated. Result being the collection contains only the only the LAST population. Assuming id is unique the each collection would contain only 1 record. And now that brings us to the heart of the problem.
The error ORA-04091: <table name> is mutating, trigger/function may not see it results from attempting to SELECT from the table that fired the trigger; this is invalid. In this case the trigger fired due to a DML action on the persons table as a result you cannot select from persons in a row level trigger (stand alone or row level part of a compound trigger. But it is not needed. The pseudo rows :old and :new contain the complete image of the row. To get a value just reference the appropriate row and column name. Assign that to your collection.
Taking all into account we arrive at:
create or replace trigger personssalutation
for update
on persons
compound trigger
type persons_table is table of
persons.salutationtitle%type;
gold persons_table := persons_table();
gnew persons_table := persons_table();
before each row is
begin
gold.extend;
gold(gold.count) := :old.salutationtitle;
end before each row;
after each row is
begin
gnew.extend;
gold(gold.count) := :new.salutationtitle;
end after each row;
after statement is
begin
for i in 1 .. gnew.count loop
state_00.salutations_todelete(state_00.salutations_todelete.count + 1) := gold(i);
state_00.salutations_toinsert(state_00.salutations_toinsert.count + 1) := gnew(i);
end loop;
end after statement;
end personssalutation;
NOTE: Unfortunately you did not provide sample data, nor description of the functions in the AFTER STATEMENT section. Therefore the above is not tested.
Hello fellow programmers and happy new year to you all!
I have few university tasks for winter break and one of them is to create trigger on table:
PERSON(ID, Name, Surname, Age);
Trigger is supposed to inform user when they have inserted row with invalid ID. Vadility criteria is that ID is 11 digits long.
I tried to write solution like this:
CREATE OR REPLACE TRIGGER person_id_trigg
AFTER INSERT
ON person
DECLARE
idNew VARCHAR(50);
lengthException EXCEPTION;
BEGIN
SELECT id INTO idNew FROM INSERTED;
IF LENGTH(idNew) <> 11 THEN
RAISE lengthException;
END IF;
EXCEPTION
WHEN lengthException THEN
dbms_output.put_line('ID for new person is INVALID. It must be 11 digits long!');
END;
Then I realized that INSERTED exists only in sqlserver and not in oracle.
What would you suggest I could do to fix that?
Thanks in advance!
Do you want to raise an exception (which would prevent the insert from succeeding)? Or do you want to allow the insert to succeed and write a string to the dbms_output buffer that may or may not exist and may or may not be shown to a human running the insert?
In either case, you'll want this to be a row-level trigger, not a statement-level trigger, so you'll need to add the for each row clause.
CREATE OR REPLACE TRIGGER person_id_trigg
AFTER INSERT
ON person
FOR EACH ROW
If you want to raise an exception
BEGIN
IF( length( :new.id ) <> 11 )
THEN
RAISE_APPLICATION_ERROR( -20001,
'The new ID value must have a length of 11' );
END IF;
END;
If you want to potentially print output but allow the insert to succeed
BEGIN
IF( length( :new.id ) <> 11 )
THEN
dbms_output.put_line( 'The new ID value must have a length of 11' );
END IF;
END;
Of course, in reality, you would never use a trigger for this sort of thing. In the real world, you would use a constraint.
I create a table that has a self relation like this :
create table Organization ( id , name , parent_id)
for easy access i add organization binary code to the table , and add a trigger for update the organization binary code after change the parent.
when I want to update then i make recursive loop and take dead lock from oracle.
I update the code in a separate function and call it at the end of trigger;
I make the loop like this:
Update The Record
Run The trigger
Update The Organization Code
Run The trigger
update The Organization Code
and so on
CREATE OR REPLACE TRIGGER TRG_Core_Organization_before
before insert OR UPDATE on Core_Organization
for each row
declare
-- local variables here
parent_HIERARCHICODE number;
PRAGMA AUTONOMOUS_TRANSACTION;
begin
IF INSERTING OR (UPDATING AND :new.parentid != :old.parentid) OR
(UPDATING AND :old.hierarchicode IS NULL) THEN
Begin
-- get last code in this root
-- RAISE_APPLICATION_ERROR(-20001,'ERROR HIERARCHICODE');
select HIERARCHICODE
into parent_HIERARCHICODE
from Core_Organization cp
where cp.id = :new.parentid;
select NVL(max(HIERARCHICODE) + 1, parent_HIERARCHICODE || '001')
into :new.hierarchicode
from Core_Organization cp
where cp.parentid = :new.parentid;
EXCEPTION
WHEN NO_DATA_FOUND THEN
:new.hierarchicode := cast(parent_HIERARCHICODE || '001' as number);
END;
END IF;
end;
I have this simple table called Favorites.
Favorites
| username | type_of_movie | like_or_dislike |
The data looks like this :
AAA, Action, Like
AAA, Romance, Dislike
...
I have made a trigger to count the maximum favorite types and prevent the user to like all the genre.
CREATE OR REPLACE TRIGGER trgLike
BEFORE INSERT OR UPDATE ON Favorite
FOR EACH ROW
DECLARE
count number;
BEGIN
SELECT
COUNT(username) INTO count
FROM
Favorite
WHERE
username= :NEW.username AND like_or_dislike = 'Like';
IF (count = 3) THEN
RAISE_APPLICATION_ERROR(-20000,'Too much liking');
END IF;
END;
/
I want the users just to be able to like 3 genre of movies.
The insert trigger works pretty well but when I try to update something to dislike, I get and error ORA-04091 table is in mutation. Error at line 6.
How can I prevent this? I have searched and it seems that my update will change the value of my select but I don't see how.
I am using Oracle version 11g.
The error message appears, because your trigger queries the Favorite table at the same time while table contents are changing (either UPDATE or INSERT).
In order to work around this issue you'll need three triggers and a small package:
CREATE OR REPLACE PACKAGE state_pkg AS
type ridArray IS TABLE OF rowid INDEX BY binary_integer;
newRows ridArray;
empty ridArray;
END;
/
CREATE OR REPLACE TRIGGER trgLike_clear_table
BEFORE INSERT OR UPDATE ON Favorite
BEGIN
state_pkg.newRows := state_pkg.empty;
END;
/
CREATE OR REPLACE TRIGGER trgLike_capture_affected_rows
AFTER INSERT OR UPDATE ON Favorite FOR EACH ROW
BEGIN
state_pkg.newRows(state_pkg.newRows.count +1) := :new.rowid;
END;
/
CREATE OR REPLACE TRIGGER trgLike_do_work
AFTER INSERT OR UPDATE ON Favorite
DECLARE
likes NUMBER;
BEGIN
FOR i IN 1..state_pkg.newRows.count LOOP
SELECT COUNT(*) INTO likes
FROM Favorite
WHERE username = (SELECT username FROM Favorite WHERE rowid = state_pkg.newRows(i))
AND like_or_dislike = 'Like';
IF (likes = 3) THEN
RAISE_APPLICATION_ERROR(-20000, 'Too much liking');
END IF;
END LOOP;
END;
/
There is a nice article about this at AskTom.
p.s.: see updated and tested version above.
You can use materialized view in your trigger.
Basically I need to make a for loop that will loop though the amount of rows. In each row I need to check a value and change it if it meets the requirements.
I'm new to Oracle, so I just started building it one step at a time and I'm stuck on looping through the table rows. I need to first get the number count of the rows that a Boolean flag set to 0 (false). So then I can loop through only those rows, not every row in the table. Once I'm done with whatever I need to change in that row, set the flag to 1 (true), so when I run the procedure again it won't include that row.
Here's what I have so far:
My table:
CREATE TABLE test_table_results (
name varchar,
account number,
address varchar,
database_search NUMBER(1) DEFAULT 0 NOT NULL
CONSTRAINT searched_in_database CHECK (database_search IN (0,1))
);
The table in my database:
CREATE TABLE test_table_accounts (
name varchar,
account number,
address varchar,
);
Now the procedure will go though the results table and see if the address match, if they do it will copy the account number from the database table into the results account number, then change the flag from 0 to 1, so the next time I search though the table it won't include it because it was already searched.
create or replace PROCEDURE FIND_MATCH_ADDRESS AS
BEGIN
DECLARE
v_cnt NUMBER;
BEGIN
FOR i IN (SELECT rowid, r.* FROM test_table
WHERE database_searched = 0)
LOOP
LOOP
SELECT COUNT(1) INTO v_cnt
FROM test_table
WHERE database_searched = 0;
DBMS_OUTPUT.PUT_LINE(v_cnt);
END LOOP;
END LOOP;
END;
END FIND_MATCH_ADDRESS;
EDIT: Added the two tables in hopes to make my question/task more understandable.
Again thank you for your time!!
In your example I see some mistakes.
In the procedure you do not need Declare. Declaration block is between as and begin
create or replace PROCEDURE proc
AS
-- here variable declaration or local function or procedures
BEGIN
-- here you can write a business logic
END proc;
You can iterate over the records of a table with a For loop. In your example, you also tried to use them. You can iterate over the records of a table with a For loop. In your example, you also tried to use them.
FOR record IN (cursor)
LOOP
{...statements...}
END LOOP;
I did not quite understand why you used another loop in the loop. a loop statement is an endless loop.
loop
...
end loop;
In the loop you can now implement your logic. If you really want to use a loop, then your solution might look like this
create or replace PROCEDURE FIND_MATCH_ADDRESS
AS
v_cnt NUMBER;
BEGIN
FOR rec IN (SELECT r.name
,r.address
,r.account
,a.account as new_account
FROM test_table_results r
join test_table_accounts a on a.address = r.address
WHERE r.database_searched = 0)
LOOP
update test_table_results
set account = rec.new_account
, database_searched = true
where account = rec.account
and name = rec.name
and adress = rec.adress;
END LOOP;
END FIND_MATCH_ADDRESS;
Alternatively, you can also do that with an update. Since I do not know your tables, you should then optimize the where condition.
update test_table_results t
set database_searched = true
, account = (select account
from test_table_accounts a
where a.account = t.account
and a.name = t.name
and a.adress = t.adress)
where database_searched = false
and exists(select 1
from test_table_accounts a
where a.account = t.account
and a.name = t.name
and a.adress = t.adress);