Create trigger in Oracle 11g - oracle

I want to create a trigger in Oracle 11g. The problem is that I want a trigger which runs every time when there is a SELECT statement. Is this possible or is there other way to achieve the same result. This is the PL/SQL block:
CREATE TRIGGER time_check
BEFORE INSERT OR UPDATE OF users, passwd, last_login ON table
FOR EACH ROW
BEGIN
delete from table where last_login < sysdate - 30/1440;
END;
I'm trying to implement a table where I can store user data. I want to "flush" the rows which are old than one hour. Are there other alternatives to how I could implement this?
p.s Can you tell me is this PL/SQL block is correct. Are there any mistakes?
BEGIN
sys.dbms_scheduler.create_job(
job_name => '"ADMIN"."USERSESSIONFLUSH"',
job_type => 'PLSQL_BLOCK',
job_action => 'begin
-- Insert PL/SQL code here
delete from UserSessions where last_login < sysdate - 30/1440;
end;',
repeat_interval => 'FREQ=MINUTELY;INTERVAL=2',
start_date => systimestamp at time zone 'Asia/Nicosia',
job_class => '"DEFAULT_JOB_CLASS"',
comments => 'Flushes expired user sessions',
auto_drop => FALSE,
enabled => FALSE);
sys.dbms_scheduler.set_attribute( name => '"ADMIN"."USERSESSIONFLUSH"', attribute => 'job_priority', value => 5);
sys.dbms_scheduler.set_attribute( name => '"ADMIN"."USERSESSIONFLUSH"', attribute => 'logging_level', value => DBMS_SCHEDULER.LOGGING_FAILED_RUNS);
sys.dbms_scheduler.enable( '"ADMIN"."USERSESSIONFLUSH"' );
END;

I'm not aware of a way of having a trigger on select. From the documentation, the only statements you can trigger on are insert/delete/update (and some DDL).
For what you want to do, I would suggest a simpler solution: use the DBMS_SCHEDULER package to schedule a cleanup job every so often. It won't add overhead to your select queries, so it should have less performance impact globally.
You'll find lots of examples in: Examples of Using the Scheduler

Related

Prevent record insert without mutating

I am trying to prevent inserts of records into a table for scheduling. If the start date of the class is between the start and end date of a previous record, and that record is the same location as the new record, then it should not be allowed.
I wrote the following trigger, which compiles, but of course mutates, and therefore has issues. I looked into compound triggers to handle this, but either it can't be done, or my understanding is bad, because I couldn't get that to work either. I would have assumed for a compound trigger that I'd want to do these things on before statement, but I only got errors.
I also considered after insert/update, but doesn't that apply after it's already inserted? It feels like that wouldn't be right...plus, same issue with mutation I believe.
The trigger I wrote is:
CREATE OR REPLACE TRIGGER PREVENT_INSERTS
before insert or update on tbl_classes
DECLARE
v_count number;
v_start TBL_CLASS_SCHED.start_date%type;
v_end TBL_CLASS_SCHED.end_date%type;
v_half TBL_CLASS_SCHED.day_is_half%type;
BEGIN
select start_date, end_date, day_is_half
into v_start, v_end, v_half
from tbl_classes
where class_id = :NEW.CLASS_ID
and location_id = :NEW.location_id;
select count(*)
into v_count
from TBL_CLASS_SCHED
where :NEW.START_DATE >= (select start_date
from TBL_CLASS_SCHED
where class_id = :NEW.CLASS_ID
and location_id = :NEW.location_id)
and :NEW.START_DATE <= (select end_date
from TBL_CLASS_SCHED
where class_id = :NEW.CLASS_ID
and location_id = :NEW.location_id);
if (v_count = 2) THEN
RAISE_APPLICATION_ERROR(-20001,'You cannot schedule more than 2 classes that are a half day at the same location');
end if;
if (v_count = 1 and :NEW.day_is_half = 1) THEN
if (v_half != 1) THEN
RAISE_APPLICATION_ERROR(-20001,'You cannot schedule a class during another class''s time period of the same type at the same location');
end if;
end if;
EXCEPTION
WHEN NO_DATA_FOUND THEN
null;
END;
end PREVENT_INSERTS ;
Perhaps it can't be done with a trigger, and I need to do it multiple ways? For now I've done it using the same logic before doing an insert or update directly, but I'd like to put it as a constraint/trigger so that it will always apply (and so I can learn about it).
There are two things you'll need to fix.
Mutating occurs because you are trying to do a SELECT in the row level part of a trigger. Check out COMPOUND triggers as a way to mitigate this. Basically you capture info at row level, and the process that info at the after statement level. Some examples of that in my video here https://youtu.be/EFj0wTfiJTw
Even with the mutating issue resolved, there is a fundamental flaw in the logic here (trigger or otherwise) due to concurrency. All you need is (say) three or four people all using this code at the same time. All of them will get "Yes, your count checks are ok" because none of them can see each others uncommitted data. Thus they all get told they can proceed and when they finally commit, you'll have multiple rows stored hence breaking the rule your tirgger (or wherever your code is run) was trying to enforce. You'll need to look an appropriate row so that you can controlling concurrent access to the table. For an UPDATE, that is easy because this means there is already some row(s) for the location/class pairing. For an INSERT, you'll need to ensure an appropriate unique constraint is in place on a parent table somewhere. Hard to say without seeing the entire model
In principle a compound trigger could be this one:
CREATE OR REPLACE TYPE CLASS_REC AS OBJECT(
CLASS_ID INTEGER,
LOCATION_ID INTEGER,
START_DATE DATE,
END_DATE DATE,
DAY_IS_HALF INTEGER
);
CREATE OR REPLACE TYPE CLASS_TYPE AS TABLE OF CLASS_REC;
CREATE OR REPLACE TRIGGER UIC_CLASSES
FOR INSERT OR UPDATE ON TBL_CLASSES
COMPOUND TRIGGER
classes CLASS_TYPE;
v_count NUMBER;
v_half TBL_CLASS_SCHED.DAY_IS_HALF%TYPE;
BEFORE STATEMENT IS
BEGIN
classes := CLASS_TYPE();
END BEFORE STATEMENT;
BEFORE EACH ROW IS
BEGIN
classes.EXTEND;
classes(classes.LAST) := CLASS_REC(:NEW.CLASS_ID, :NEW.LOCATION_ID, :NEW.START_DATE, :NEW.END_DATE, :NEW.DAY_IS_HALF);
END BEFORE EACH ROW;
AFTER STATEMENT IS
BEGIN
FOR i IN classes.FIRST..classes.LAST LOOP
SELECT COUNT(*), v_half
INTO v_count, v_half
FROM TBL_CLASSES
WHERE CLASS_ID = classes(i).CLASS_ID
AND LOCATION_ID = classes(i).LOCATION_ID
AND classes(i).START_DATE BETWEEN START_DATE AND END_DATE
GROUP BY v_half;
IF v_count = 2 THEN
RAISE_APPLICATION_ERROR(-20001,'You cannot schedule more than 2 classes that are a half day at the same location');
END IF;
IF v_count = 1 AND classes(i).DAY_IS_HALF = 1 THEN
IF v_half != 1 THEN
RAISE_APPLICATION_ERROR(-20001,'You cannot schedule a class during another class''s time period of the same type at the same location');
end if;
end if;
END LOOP;
END AFTER STATEMENT;
END;
/
But as stated by #Connor McDonald, there are several design flaws - even in a single user environment.
A user may update the DAY_IS_HALF, I don't think the procedure covers all variants. Or a user updates END_DATE and by that, the new time intersects with existing classes.
Better avoid direct insert into the table and create a PL/SQL stored procedure in which you perform all the validations you need and then, if none of the validations fail, perform the insert. And grant execute on that procedure to the applications and do not grant applications insert on that table. That is a way to have all the data-related business rules in the database and make sure that no data that violates those rules in entered into the tables, no matter by what client application, for any client application will call a stored procedure to perform insert or update and will not perform DML directly on the table.
I think the main problem is the ambiguity of the role of the table TBL_CLASS_SCHED and the lack of clear definition of the DAY_IS_HALF column (morning, afternoon ?).
If the objective is to avoid 2 reservations of the same location at the same half day, the easiest solution is to use TBL_CLASS_SCHED to enforce the constraint with (start_date, location_id) being the primary key, morning reservation having start_date truncated at 00:00 and afternoon reservation having start_date set at 12:00, and you don't need end_date in that table, since each row represents an half day reservation.
The complete solution will then need a BEFORE trigger on TBL_CLASSES for UPDATE and INSERT events to make sure start_date and end_date being clipped to match the 00:00 and 12:00 rule and an AFTER trigger on INSERT, UPDATE and DELETE where you will calculate all the half-day rows to maintain in TBL_CLASS_SCHED. (So 1 full day reservation in TBL_CLASSES, will generate 2 rows in TBL_CLASS_SCHED). Since you maintain TBL_CLASS_SCHED from triggers set on TBL_CLASSES without any need to SELECT on the later, you don't have to worry about mutating problem, and because you will constraint start_date to be either at 00:00 or 12:00, the primary key constraint will do the job for you. You may even add a unique index on (start_date, classe_id) in TBL_CLASS_SCHED to avoid a classe to be scheduled at 2 locations at the same time, if you need to.

How to analyze a table using DBMS_STATS package in PL/SQL?

Here's the code I'm working on:
begin
DBMS_STATS.GATHER_TABLE_STATS (ownname => 'appdata' ,
tabname => 'TRANSACTIONS',
cascade => true,
estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE,
method_opt=>'for all indexed columns size 1',
granularity => 'ALL',
degree => 1);
end;
After executing the code, PL/SQL procedure successfully completed is displayed.
How to view the statistics for the particular table, analyzed by DBMS_STATS ?
You may see information in DBA_TABLES
SELECT *
FROM DBA_TABLES where table_name='TRANSACTIONS';
e.g. Column LAST_ANALYZED shows when it was last analyzed.
There are also information column by column in
SELECT * FROM all_tab_columns where table_name='TRANSACTIONS';
where you could find min value, max value, etc.

How to find the elapse time user was logged into database using trigger

I am trying to calculate the total time user logged into the database using a trigger
my table structure is seen below:
create table stats$user_log
(
user_id varchar2(30),
session_id number(8),
host varchar2(30),
logon_day date,
logon_time varchar2(10),
logoff_day date,
logoff_time varchar2(10),
elapsed_minutes varchar2(32)
);
My trigger for logon is as follows:
create or replace trigger
logon_audit_trigger
AFTER LOGON ON DATABASE
BEGIN
insert into stats$user_log values(
user,
sys_context('USERENV','SESSIONID'),
sys_context('USERENV','HOST'),
sysdate,
to_char(sysdate, 'hh24:mi:ss'),
null,
null,
null
);
END;
/
My trigger for logoff is as follows:
create or replace trigger
logoff_audit_trigger
BEFORE LOGOFF ON DATABASE
BEGIN
UPDATE
stats$user_log
set
logoff_day = sysdate,
logoff_time = to_char(sysdate, 'hh24:mi:ss'),
elapsed_minutes = round((logoff_day - logon_day)*1440,2)
WHERE
sys_context('USERENV','SESSIONID') = session_id;
END;
/
When the user logs out everything is captured except the elapse_minutes column it remains as null.
Can anyone tell me where i'm going wrong please and thanks
At the time you do the update, the logoff_day you refer to in the right-hand side of the set expression is still null, so the expression evaluates to null.
Any column values you refer to have to be the pre-update values, or changing the order that the columns are assigned within the set clause would change how the update worked, which at best be confusing. An update that sets a column based on its old value - e.g. set salary = salary * 1.1 - would be particularly problematic.
You can refer to sysdate a second time instead:
logoff_day = sysdate,
logoff_time = to_char(sysdate, 'hh24:mi:ss'),
elapsed_minutes = round((sysdate - logon_day)*1440,2)
If session auditing is enabled, the database already does this for you. Why create that for yourself? Check dba_audit_session for the results. You might need to talk to your dba / security staff to get access but it might be worth it.

How to audit deletes in a certain table with Oracle?

I'm trying to record DELETE statements in a certain table using Oracle's auditing features. I ran:
SQL> AUDIT DELETE TABLE BY TPMDBO BY ACCESS;
Audit succeeded.
I'm unclear if this audits the deletion of a table schema itself (ie, dropping the table), or if it audits the deletion of one or more rows within any table (ie, the delete command). If the latter, how do I limit this auditing to only a table called Foo? Thanks!
UPDATE:
SQL> show parameter audit
NAME TYPE VALUE
------------------------------------ ----------- -------------
audit_file_dest string /backup/audit
audit_sys_operations boolean TRUE
audit_syslog_level string
audit_trail string XML, EXTENDED
There is a new feature called fine-grained auditing (FGA), that stores log in SYS.FGA_LOG$ instead SYS.AUD$. Here is the FGA manual.
BEGIN
DBMS_FGA.ADD_POLICY(
object_schema => 'HR',
object_name => 'FOO',
policy_name => 'my_policy',
policy_owner => 'SEC_MGR',
enable => TRUE,
statement_types => 'DELETE',
audit_condition => 'USER = ''myuser''',
audit_trail => DBMS_FGA.DB);
END;
/
Yes, your original command should audit DELETE operations (not DROP) for this user on all tables. Examine show parameter audit

Oracle: how to run a stored procedure "later"

We have a system that allows users interfacing data into the database to set up various rules that are used to alter data before it is merged in to the main table. For example, an order might have a rule that sets up what delivery company to use based on a customer's address.
This is originally intended to operate only on the data being loaded in, so it's limited to functions that you can call from a select statement. An important point to note is that the data is not in the destination table yet.
Now, I have a project that requires an update to another table (fine - I can use an autonomous_transaction pragma for that). However, there are some functions I need to run that require the data to be inserted before they run (i.e. they are aggregating data).
So, I really want to just queue up running my procedure till some time later (it's not time dependent).
How do I do that in Oracle? The wealth of documentation is rather overwhelming when I just want to do something simple.
BEGIN
DBMS_SCHEDULER.create_job (
job_name => 'daily_tasks_job',
job_type => 'STORED_PROCEDURE',
job_action => 'prc_daily_tasks',
repeat_interval => 'FREQ=DAILY; INTERVAL=1',
enabled => TRUE,
comments => 'Calls stored procedure once a day'
);
END;
BEGIN
DBMS_SCHEDULER.create_job(
job_name => 'SHELL_JOB',
repeat_interval => 'FREQ=DAILY; BYHOUR=2',
job_type => 'EXECUTABLE',
job_action => '/u01/app/oracle/admin/tools/shell_job.sh',
enabled => TRUE,
comments => 'Perform stuff'
);
END;
The standard aproach for this would be to use dbms_jobs to schedule a job calling the procedure.
If there is some precondition, the job could check the precondition. If it is fulfilled, the job continues, if not it reschedules itself and exit.

Resources