Oracle DB: Email Trigger - oracle

I am writing a trigger for an Oracle DB to send an email then update a column for that record acknowledging that the email has been sent. I was advised to create a cursor to fetch each row, then gather the info for the email, send the email, update the record, then repeat in a loop. The code below is what I have so far.
CREATE OR REPLACE TRIGGER "SEND_EMAIL"
After INSERT OR UPDATE OF ISSUE_ADDED_TO_ALM ON DB_TABLE FOR EACH ROW
DECLARE
l_table DB_TABLE%rowtype;
l_body varchar2(4000);
l_to_address varchar2(2000);
l_from varchar2(200);
l_name varchar2(100);
l_summary varchar2(1000);
l_description varchar2(4000);
l_ALM_ID varchar2(100);
l_subject varchar2(400);
l_added_to_alm varchar2(200);
l_SID varchar2(200);
CURSOR cur_ADDED_TO_ALM IS
select * FROM DB_TABLE where ISSUE_ADDED_TO_ALM = '1' and EMAIL_NOTIFICATION = '0';
BEGIN
OPEN cur_ADDED_TO_ALM;
LOOP
Fetch cur_ADDED_TO_ALM into l_table;
Exit when cur_ADDED_TO_ALM%NOTFOUND;
l_from := 'Data Quality IMS Team';
select ISSUE_REQUESTER into l_SID from DB_TABLE;
select emp_email_name,concat(concat(emp_first_name,' '),emp_last_name) into l_to_address, l_name from telephone_book where emp_id = l_SID;
select ISSUE_SUMMARY,ISSUE_DESCRIPTION,ALM_ISSUE_ID into l_summary, l_description, l_ALM_ID from DB_TABLE where ISSUE_ADDED_TO_ALM = '1' and EMAIL_NOTIFICATION = '0';
l_subject := l_ALM_ID + 'Request has been created.';
l_body := '<style type="text/css">
p{font-family: Calibri, Arial, Helvetica, sans-serif;
font-size:12pt;
margin-left:30px;
}
</style>';
l_body := l_body || '<p>Your request has been created.</p>';
l_body := l_body || '<p>Data Quality Center received the following request:</p>';
l_body := l_body || '<p>Request ID: '|| l_ALM_ID ||'</p>';
l_body := l_body || '<p>Request Title: '|| l_summary||'</p>';
l_body := l_body || '<p>Request Description: '|| l_description||'</p>';
HTMLDB_MAIL.SEND(
P_TO => l_to_address,
P_FROM => l_from,
P_BODY => l_body,
P_BODY_HTML => l_body,
P_SUBJ => l_subject);
wwv_flow_mail.push_queue(
P_SMTP_HOSTNAME => 'mail.sever_name.net',
P_SMTP_PORTNO => '5'
);
END LOOP;
update DB_TABLE set EMAIL_NOTIFICATION = '1' where ALM_ISSUE_ID = l_ALM_ID
IF cur_ADDED_TO_ALM%ISOPEN then
CLOSE cur_ADDED_TO_ALM;
END IF;
end;
I get the following error:
ORA-04091: table server.DB_Table is mutating, trigger/function may not see it ORA-06512: at "server.SEND_EMAIL", line 15 ORA-06512: at "server.SEND_EMAIL", line 18 ORA-04088: error during execution of trigger 'server.SEND_EMAIL'

I offer these solutions.
You cannot UPDATE the table upon which the trigger is activating. You should instead set the value using :new.EMAIL_NOTIFICATION := 1;
You cannot SELECT the table upon which the trigger is activating. This logically doesn't make sense. That's the reason you're getting a "mutating table" error. The table is changing (mutatiing) and you want to read it, which would produce ambiguous results. DO NOT GO AROUND THIS WITH AN AUTONOMOUS TRANSACTION. Someone somewhere is going to give you this suggestion. That would be digging yourself even deeper into a hole.
There is no need for the explicit cursor. Use an implicit one for clarity and simplicity.
Overall, this is a terrible idea. For a purpose such as this you really don't want to do it this way. Think about it, the user does an insert or update and must wait until the database sends all the necessary emails. If there are any exceptions raised in the trigger the transaction fails and you have to roll back.
Better plans
Have the trigger place the email requests into a table. Have a job that runs frequently to send these emails to the email server in the background so the user doesn't wait.
Have the trigger place email-like messages into a queue. Have a job that ....
Avoid the trigger completely. Just have a job which identifies these records and ....

Related

Oracle Apex - Best way to send Email Interactive Grid

I have an Interactive Grid on my page, the idea is that every time the user inserts / updates rows an email will be send to the Admins for audit checks (manual back-office process).
This is the code (Trigger) I have so far for the insert, however, it is not the best option, since it only captures the last - new - Id:
create or replace trigger "my_email_trigger"
after insert on "my_table"
for each row
DECLARE
l_body CLOB;
v_user varchar2(300);
V_REQUESTER varchar2(300);
v_max number;
begin
l_body := 'Hi ' || INITCAP(replace(regexp_replace(:NEW."User_email",'#[a-zA-z0-9.]*',''),
'.', ' ')) ||',' || utl_tcp.crlf;
l_body := l_body || utl_tcp.crlf;
l_body := l_body || 'Please review new Row ID: '|| :NEW."ID" ||utl_tcp.crlf;
l_body := l_body || utl_tcp.crlf;
l_body := l_body || utl_tcp.crlf;
l_body := l_body || 'Thank you' || utl_tcp.crlf;
apex_mail.send(
p_to => :NEW."User_email",
p_from => 'noreply#mail.com',
p_body => l_body,
p_subj => 'Please review new Row Id' );
END;
If the user adds 3 new rows, this trigger would send 3 different emails.
Is there a way to capture all 3 inserted rows (for example) on a single email?
Thanks!
Instead of using a trigger on the table - a trigger will fire for every affected row and that will not work - I suggest you use a page process.
Check this post which is pretty similar. Note that the page process is also fired for every row so you'll probably need 2 page processes
One page process on the IG Editable region to populate an item with the list of affected row ids. Since this is executed for every affected row you could just concatenate values and this process cannot send the email because you'd have the same issue as the trigger: one mail per row.
Another page process that just sends the email if the page item from the first process is not null.
Another advantage over trigger is that this keeps you business functionality in the application code. If you move that to triggers isn't that visible anymore.

TRIGGER BEFORE A COMMIT (INSERT OR UPDATE)

I have a trigger that calls a webservice link.
This link read a View, and that view is composing a XML.
The problema is:
The trigger is executed when I have a cod_situation = 6 like this:
CREATE OR REPLACE TRIGGER trg_candidato_chama_link
AFTER INSERT OR UPDATE ON cand_proc_sel
FOR EACH ROW DECLARE
v_url VARCHAR2(4000);
req UTL_HTTP.REQ;
resp UTL_HTTP.RESP;
value VARCHAR2(1536);
--temp
v_count INTEGER;
v_alternativas VARCHAR2(1000);
v_error VARCHAR2(4000);
BEGIN
if (:new.cod_situation = 6 ) THEN
v_url := 'http://.../frameweb/amxv7/amx_new_employee';
req := UTL_HTTP.BEGIN_REQUEST(v_url);
UTL_HTTP.SET_HEADER(req, 'User-Agent', 'Mozilla/4.0');
resp := UTL_HTTP.GET_RESPONSE(req);
LOOP
UTL_HTTP.READ_LINE(resp, value, TRUE);
DBMS_OUTPUT.PUT_LINE(value);
END LOOP;
UTL_HTTP.END_RESPONSE(resp);
END IF;
EXCEPTION WHEN UTL_HTTP.END_OF_BODY THEN
UTL_HTTP.END_RESPONSE(resp);
INSERT INTO integratio_log(data, cod_integracao, status, rotina, obs) VALUES(SYSDATE, 7, 'SUCCESS', 'TEST DOM, TRIGGER URL', 'Url : ' || v_url || ' count test: '|| v_count); WHEN OTHERS THEN
v_error := To_Char(SQLERRM);
INSERT INTO integracao_log(data, cod_integracao, status, rotina, obs) VALUES(SYSDATE, 7, 'FAIL', 'TEST DOM, TRIGGER URL', 'Url : ' || v_url || ' # qtd: '|| v_count || ' # Erro: ' || v_error);
END;
But, the problem is:
the clause where in the View is just the condition cod_situation = 6, but when the trigger calls the webservice where we read the View, I do not have cod_situation = 6 yet.
So, my question is, how can I call the trigger link, but only after the commit is done in the table?
A post commit hook is not available. And this is for good reason: At the point where you access the web service the modified data has already been committed. What should the database do if the commit is successful but the post commit fails?
Generally speaking it is not a good idea to access other systems from a trigger: Your session is still not committed hence a rollback is possible. That would means that your web service has been informed about a transaction in the database that might never be written to disk. This may or may not be catastrophic. Additionally you make the functionality of your database dependent on the availability of the web service which is bad practice in itself.
If you absolutely have to do what you are doing you would do yourself a favor by moving the view reading into a stored procedure and call that from the trigger and the web service.

Oracle, trigger to control link between tables

I need to create a trigger to control a link between tables. I have the following table structure (image below). an item is linked to a budget and a structure, and this structure should be linked to the same budget.
Sometimes the item receive wrong structures, receive structures different from its own budget budget. I need that when you insert a budget item or edit a budget item to be validated if the id_budget are equal, the id_budget of dg_budget_item is equal to the id_budget dg_budget_structure to which it is being linked
I started building this trigger, but do not know how to continue
CREATE TRIGGER T_BUDGETO_STRUCTURE_ITEM_ID_STRUCTURE
BEFORE INSERT OR UPDATE OF id_budget_structure ON dg_budget_structure_item
select id_budget from dg_budget_structure where id_budget_structure = new.id_budget_structure
FOR EACH ROW
WHEN (new.id_budget <> ???(result of select?))
pl/sql_block
This should get you close to what you want
CREATE OR REPLACE TRIGGER T_BUDGET_STRUCTURE_ITEM_ID_STRUCTURE
BEFORE INSERT OR UPDATE OF id_budget_structure ON dg_budget_item
FOR EACH ROW
DECLARE
v_id_budget_structure NUMBER;
Budgets_do_not_match EXCEPTION;
v_code NUMBER;
v_errm VARCHAR2(64);
BEGIN
SELECT id_budget INTO v_id_budget_structure FROM dg_budget_structure WHERE id_budget_structure = :new.id_budget_structure
IF :new.id_budget <> v_id_budget_structure THEN
RAISE Budgets_do_not_match;
END IF;
EXCEPTION
WHEN Budgets_do_not_match THEN
Raise_application_error (-20001,
'Budget '||TO_CHAR(:new.id_budget)||' for structure '
|| :new.id_budget_structure || 'does not match the budget trying to be linked';
WHEN NO_DATA_FOUND THEN
Raise_application_error(-20002,
'Invalid budget structure ' ||:new.id_budget_structure);
WHEN OTHERS THEN
v_code := SQLCODE;
v_errm := SUBSTR(SQLERRM, 1 , 64);
Raise_application_error(-20000,
'Unexpected error ' || v_code ': ' || v_errm;
END;
You cannot ROLLBACK within a trigger so your application will need to deal with that.

When to re-raise the same exception in Oracle

I'm reading Steven Feuerstein's PL/SQL book. According to it:
Use this form when you want to re-raise (or propagate out) the same
exception from within an exception handler, as you see here:
EXCEPTION
WHEN NO_DATA_FOUND
THEN
-- Use common package to record all the "context" information,
-- such as error code, program name, etc.
errlog.putline (company_id_in);
-- And now propagate NO_DATA_FOUND unhandled to the enclosing block.
RAISE;
This feature is useful when you want to log the fact that an error
occurred, but then pass that same error out to the enclosing block.
That way, you record where the error occurred in your application but
still stop the enclosing block(s) without losing the error
information.
I give it a try:
create table log_error
(
error_code number,
error_name varchar2(400)
);
declare
l_q number := 400;
l_r number := 0;
l_result number;
err_num NUMBER;
err_msg VARCHAR2(100);
begin
l_result := l_q/l_r;
EXCEPTION
WHEN OTHERS THEN
err_num := SQLCODE;
err_msg := SUBSTR(SQLERRM, 1, 100);
insert into log_error values (err_num , err_msg);
end;
select * from log_error;
I got below data in my log_error table:
-1476 ORA-01476: divisor is equal to zero
Now I place raise in my exception block:
declare
l_q number := 400;
l_r number := 0;
l_result number;
err_num NUMBER;
err_msg VARCHAR2(100);
begin
l_result := l_q/l_r;
EXCEPTION
WHEN OTHERS THEN
err_num := SQLCODE;
err_msg := SUBSTR(SQLERRM, 1, 100);
insert into log_error values (err_num , err_msg);
raise;
end;
Now when I run this block, i got nothing in my log table and also I got the error.
Error report -
ORA-01476: divisor is equal to zero
ORA-06512: at line 14
01476. 00000 - "divisor is equal to zero"
What is the use of raise? When I have to use this?
Your logging process needs to be a little different. When an exception is called all data/transactions that are not committed are rolled back. You can add more details with the newer features Oracle has added.
err_msg := DBMS_UTILITY.FORMAT_ERROR_STACK()||DBMS_UTILITY.FORMAT_ERROR_BACKTRACE();
You need to create a logging procedure which uses the PRAGMA AUTONOMOUS TRANSACTION. Pass in the SQLCODE and err_msg with the details and this will log the error no matter what. Here is what I use which also uses Feurstein's Q Error package. This link to the Q$Error package is quite informative.
PROCEDURE LOG (err_in IN INTEGER:= SQLCODE,
msg_in IN VARCHAR2:= NULL,
vlocation_in IN VARCHAR2:= NULL)
IS
/******************************************************************************
PURPOSE: log a code error, business logic error or information message in APPLICATION_ERROR_LOGGING
we want the error message to be logged even if the calling transaction fails or hangs
******************************************************************************/
PRAGMA AUTONOMOUS_TRANSACTION;
v_err_text VARCHAR2 (4000) := SQLERRM;
BEGIN
v_err_text := v_err_text || ' ' || GET_MORE_ERROR_DESCRIPTION (err_in);
INSERT INTO application_error_logging (ID,
request_uri,
ERROR_CODE,
user_id,
stack_trace,
information,
"TIMESTAMP")
VALUES (application_error_logging_seq.NEXTVAL,
vlocation_in,
TO_CHAR (err_in),
g_admin_id,
msg_in,
v_err_text,
localtimestamp);
COMMIT;
EXCEPTION
WHEN OTHERS
THEN
q$error_manager.raise_error (
error_code_in => SQLCODE,
text_in => SQLERRM,
name1_in => 'LOCATION',
value1_in => 'APP_UTIL.LOG',
name2_in => 'v_location',
value2_in => vlocation_in,
name3_in => 'err_in',
value3_in => TO_CHAR (err_in)
);
END LOG;
Apparently, Steven Feuerstein's procedure errlog.putline() uses an autonomous transaction to insert the record into the log table. In your case you perform the insert in the same transaction, which is rolled back by the caller when the exception is re-raised.

Oracle trigger checks previous record before insert/update

I would like to create a trigger that prevents a student from enrolling into a new module if he has any outstanding bills.
studID studNRIC paymentStatus
-------------------------------------
200 F7654672F Non Payment
it would reject the following statement:
INSERT INTO student(studID, studNRIC, paymentStatus)
VALUES (201, 'F7654672F', 'Good');
I've came out with the following trigger but I'm still able to insert a new student.
set define off;
CREATE OR REPLACE TRIGGER reject_new_account
AFTER INSERT OR UPDATE ON Student
FOR EACH ROW
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
totover NUMBER(3);
BEGIN
SELECT COUNT (*)
INTO totover
FROM Student
WHERE :NEW.nric = student.nric
AND :NEW.paymentStatus = 'Non Payment';
IF totover > 0 THEN
RAISE_APPLICATION_ERROR ( -20002,
'Student' || :NEW.nric ||
' has outstanding bills' );
END IF;
END;
/
there's seems to be a problem with line 13 AND :NEW.paymentStatus = 'Non Payment';
so how do I go about doing this?
table structure
CREATE TABLE Student(
studID INTEGER NOT NULL,
firstName CHAR(25) NULL,
lastName CHAR(25) NULL,
NRIC CHAR(9) NOT NULL,
paymentStatus CHAR(25) Default 'Good',
CONSTRAINT stud_Pkey PRIMARY KEY (studID),
CONSTRAINT studPaymentStatus_type CHECK (PaymentStatus IN ('Late Payment', 'Non Payment', 'Good'))
);
There are a couple of issues with this trigger.
First if you want to prevent an INSERT you need to use a BEFORE trigger. An AFTER trigger will fire after the insert has successfully completed, by which point it is too late to stop the insert.
Secondly, I'm unsure about what you are trying to achieve with your SQL statement. Since the trigger is attached to the customer table you don't need to do a select on the customer table to access the record data. You can just do:
IF :NEW.badstatus = 'Non Payment'
THEN
RAISE_APPLICATION_ERROR ( -20002,
'Employee ' || :NEW.nric ||
' already has 5 readings' );
END IF;
You probably don't want to check if the new record has a bad status, you want to check if the existing customer records have a bad status. As James mentioned, a BEFORE trigger probably makes more sense, but that depends on what you are trying to do, whether the insert gets rollback or not by the caller, etc.
CREATE OR REPLACE TRIGGER reject_new_account
AFTER INSERT OR UPDATE ON Customer
FOR EACH ROW
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
totover NUMBER(3);
BEGIN
SELECT COUNT (*)
INTO totover
FROM Customer c
WHERE c.nric = :NEW.nric
AND c.badstatus = 'Non Payment';
IF totover > 0 THEN
RAISE_APPLICATION_ERROR ( -20002,
'Employee ' || :NEW.nric ||
' already has 5 readings' );
END IF;
END;
/
I've removed the PRAGMA AUTONOMOUS_TRANSACTION; for you and replace it with an exception to handle the issue, can you try if it works.
set define off;
CREATE OR REPLACE TRIGGER reject_new_account
BEFORE INSERT OR UPDATE ON CUSTOMER
FOR EACH ROW
DECLARE
totover CHAR(100);
BEGIN
SELECT distinct badStatus
INTO totover
FROM customer
WHERE :NEW.nric = CUSTOMER.nric;
IF totover = 'Non Payment' THEN
RAISE_APPLICATION_ERROR ( -20003,
'Customer ' || :NEW.firstName||''||:NEW.lastName ||
' cannot create new account due to bad payment' );
END IF;
EXCEPTION
WHEN NO_DATA_FOUND
THEN DBMS_OUTPUT.PUT_LINE(TO_CHAR(SQLERRM(-20299)));
END;
/
As I assume you have discovered, you cannot select from the same table that a row-level trigger is defined against; it causes a table mutating exception. You have attempted to get round this by adding the autonomous transaction pragma. Unfortunately, although this works, it is just covering up your mistake in methodology.
In order to properly create this validation using a trigger a procedure should be created to obtain user-specified locks so the validation can be correctly serialized in a multi-user environment.
PROCEDURE request_lock
(p_lockname IN VARCHAR2
,p_lockmode IN INTEGER DEFAULT dbms_lock.x_mode
,p_timeout IN INTEGER DEFAULT 60
,p_release_on_commit IN BOOLEAN DEFAULT TRUE
,p_expiration_secs IN INTEGER DEFAULT 600)
IS
-- dbms_lock.allocate_unique issues implicit commit, so place in its own
-- transaction so it does not affect the caller
PRAGMA AUTONOMOUS_TRANSACTION;
l_lockhandle VARCHAR2(128);
l_return NUMBER;
BEGIN
dbms_lock.allocate_unique
(lockname => p_lockname
,lockhandle => p_lockhandle
,expiration_secs => p_expiration_secs);
l_return := dbms_lock.request
(lockhandle => l_lockhandle
,lockmode => p_lockmode
,timeout => p_timeout
,release_on_commit => p_release_on_commit);
IF (l_return = 1) THEN
raise_application_error(-20001, 'dbms_lock.request Timeout');
ELSIF (l_return = 2) THEN
raise_application_error(-20001, 'dbms_lock.request Deadlock');
ELSIF (l_return = 3) THEN
raise_application_error(-20001, 'dbms_lock.request Parameter Error');
ELSIF (l_return = 5) THEN
raise_application_error(-20001, 'dbms_lock.request Illegal Lock Handle');
ELSIF (l_return not in (0,4)) THEN
raise_application_error(-20001, 'dbms_lock.request Unknown Return Value ' || l_return);
END IF;
-- Must COMMIT an autonomous transaction
COMMIT;
END request_lock;
This procedure can then be used in a compound trigger (assuming at least Oracle 11, this will need to be split into individual triggers in earlier versions)
CREATE OR REPLACE TRIGGER reject_new_account
FOR INSERT OR UPDATE ON student
COMPOUND TRIGGER
-- Table to hold identifiers of inserted/updated students
g_studIDs sys.odcinumberlist;
BEFORE STATEMENT
IS
BEGIN
-- Reset the internal student table
g_studIDs := g_studIDs();
END BEFORE STATEMENT;
AFTER EACH ROW
IS
BEGIN
-- Store the inserted/updated students; the payment status may be updated
-- without checking the constraint
IF ( INSERTING
OR ( UPDATING
AND ( :new.studID <> :old.studID
OR :new.NRIC <> :old.NRIC)))
THEN
g_studIDs.EXTEND;
g_studIDs(g_studIDs.LAST) := :new.studID;
END IF;
END AFTER EACH ROW;
AFTER STATEMENT
IS
CURSOR csr_students
IS
SELECT sdt.studID
, sdt.NRIC
FROM TABLE(g_studIDs) sid
INNER JOIN student sdt
ON (sdt.studID = sid.column_value)
ORDER BY sdt.NRIC;
CURSOR csr_constraint_violations
(p_studID student.studID%TYPE
,p_NRIC student.studNRIC%TYPE)
IS
SELECT NULL
FROM student sdt
WHERE sdt.NRIC = p_NRIC
AND sdt.paymentStatus = 'Bad Payment'
AND sdt.studID <> p_studID;
r_constraint_violation csr_constraint_violations%ROWTYPE;
BEGIN
-- Check if for any inserted/updated student there exists another record for
-- the same NRIC with a Bad Payment status. Serialise the constraint for each
-- NRIC so concurrent transactions do not affect each other
FOR r_student IN csr_students LOOP
request_lock('REJECT_NEW_ACCOUNT_' || r_student.NRIC);
OPEN csr_constraint_violations(r_student.studID, r_student.NRIC);
FETCH csr_constraint_violations INTO r_constraint_violation;
IF csr_constraint_violations%FOUND THEN
CLOSE csr_constraint_violations;
raise_application_error(-20001, 'Student ' || r_student.NRIC || ' has Bad Payment status');
ELSE
CLOSE csr_constraint_violations;
END IF;
END LOOP;
END AFTER STATEMENT;
END;

Resources