autonomous_transaction in trigger and constraint violation - oracle

I've encountered an interesting situation experimenting with the autonomous_transaction. Consider the following situation (please note it's is not intended to be written this way: just proof of concept):
create table t
(
id int primary key,
changed date
)
/
create or replace trigger t_trig
before insert or update
on t
for each row
declare
PRAGMA AUTONOMOUS_TRANSACTION;
begin
:new.changed := sysdate;
commit;
end;
/
insert into t(id, changed) values (1, sysdate);
insert into t(id, changed) values (2, sysdate);
The changed date as of current time:
SQL> select * from t;
ID CHANGED
--------- -----------------
1 19.09.11 15:29:44
2 19.09.11 15:32:35
Let's take a 5 sec break and then do the following:
update t set id = 2 where id = 1;
Obviously it will fail with constraint violation, but it doesn't change the changed attribute as well:
SQL> select * from t;
ID CHANGED
--------- -----------------
1 19.09.11 15:29:44
2 19.09.11 15:32:35
My question is: why is this happening? I'm sure I misunderstand some basic concepts but I can't get the idea.
Thanks in advance for your help.

The PRAGMA AUTONOMOUS TRANSACTION saves the context, opens another session and makes something. Commit is a must, because otherwise the changes will be lost. You can understand that only the changes made in some block in the database makes sense in this session (autonomous).
So, in your trigger you do nothing.
That variable, :new.changed is "changed" in another session, if we can say it in this mode. It is not changed for your update.

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.

Trigger to update week of the year

I want to write a trigger so that when decom_date is inserted or updated the week of the year is updated to the corresponding value.
This is what I have so far, but after inserting a date the week is still null.
create or replace trigger test_trigger
before insert on check_decom
for each row
begin
if inserting then
update check_decom set decom_week= (select to_char(to_date(decom_date,'DD-
MON-YY'),'WW') as week from check_decom) ;
end if;
end;
/
SQL> select * from check_decom;
DECOM_DATE DECOM_WEEK
------------------------------ ----------
23-JUN-17
What am I doing wrong?
Example for Week of a year
SQL> select to_char(to_date(sysdate,'DD-MON-YY'),'WW') as week from dual;
WE
--
28
You're doing a couple of things wrong, starting with date handling. Your decom_date column should be defined as a DATE column - it looks like it might be a string in your sample output. But your handling with sysdate is also wrong, as you're implicitly converting to a string in order to convert it back to a date, which is both pointless and prone to error as this might happen in a session which has different NLS settings. If your column is actually a DATE then you should not be calling to_date() against that either; and if it is a string then that conversion is valid but it should be a DATE.
Then your trigger is querying and trying to update the table that the trigger is against. With no data that doesn't error but doesn't do anything as there is no existing row to update - the one you are inserting doesn't exist yet. If there was data you would get a mutating table error, if you didn't get a too-many-rows exception from the select part.
Row-level triggers can access NEW and OLD pseudorecords to see and manipulate the affected row; you don't need to (and generally can't) use DML queries to access the data in the row you're manipulating.
If your table was defined with a date column and a number column:
create table check_decom(decom_date date, decom_week number);
then your trigger might look something like:
create or replace trigger test_trigger
before insert on check_decom
for each row
begin
if inserting then
:new.decom_week := to_number(to_char(:new.decom_date, 'WW'));
end if;
end;
/
although the if inserting check is a bit pointless as the trigger will only fire on insert anyway. Which in itself might be an issue; you perhaps want it to be set on update as well, but the logic the same, so would be:
create or replace trigger test_trigger
before insert or update on check_decom
for each row
begin
:new.decom_week := to_number(to_char(:new.decom_date, 'WW'));
end;
/
which does what you want:
insert into check_decom (decom_date) values (date '2017-06-23');
1 row inserted.
select * from check_decom;
DECOM_DAT DECOM_WEEK
--------- ----------
23-JUN-17 25
But I wouldn't do this with a trigger at all. From Oracle 11g you can use a virtual column instead:
create table check_decom (
decom_date date,
decom_week generated always as (to_number(to_char(decom_date, 'WW')))
);
Table CHECK_DECOM created.
insert into check_decom (decom_date) values (date '2017-06-23');
1 row inserted.
select * from check_decom;
DECOM_DAT DECOM_WEEK
--------- ----------
23-JUN-17 25

ORA-02437: "primary key violated" - why can't I see duplicate ID in SQL Developer?

I would receive an error:
ORA-02437: cannot validate (%s.%s) - primary key violated
Cause: attempted to validate a primary key with duplicate values or null values
I found it was because I have a stored procedure that increments the ID, but it had failed to do so when it re-ran and had an error related to one of my datatypes. I found I now had a duplicate ID in my database table. All this made sense and I was able to easily rectify it with a DELETE FROM MyTable WHERE ID = x, where x was the offending duplicate ID. The problem I have is the only way I was able to even find the IDs that were duplicated is in the first place is because I did a SELECT * FROM MyTable WHERE ID = x -- where x was one greater than the last ID I could actually see. I found it just by an educated guess. So:
Why can't I see these duplicate IDs when I open the table in Oracle SQL Developer? It only shows the last row as the ID before the duplicates. I don't think it is because of my primary key constraint, since the first line in my stored procedure is to remove that (and put it back, at the end - probably when I got my error), and it was not present when I looked at my table.
Is there some way to make these last IDs that got inserted into the table visible, so I wouldn't have to guess or assume that the duplicate IDs are "hiding" as one greater than the last ID I have in my table, in the future? There is a commit; in my stored procedure, so they should have appeared -- unless, of course, the procedure got hung up before it could run that line of code (highly probable).
Stored procedure that runs:
create or replace
PROCEDURE PRC_MYTABLE_INTAKE(
, EMPLOYEE_ID IN NVARCHAR2
, TITLE_POSITION IN NVARCHAR2
, CREATED_DATE IN DATE
, LAST_MODIFIED IN DATE
) AS
myid integer := 0;
appid integer := 0;
BEGIN
-- disable PK constraint so it can be updated
EXECUTE IMMEDIATE 'ALTER TABLE MYTABLE DROP CONSTRAINT MYTABLE_PK';
COMMIT;
-- assign ID to myid
SELECT ID INTO myid FROM MYTABLE WHERE ROWID IN (SELECT MAX(ROWID) FROM MYTABLE);
-- increment
myid := myid + 1;
-- assign APPLICATION_ID to appid
SELECT APPLICATION_ID INTO appid FROM MYTABLE WHERE ROWID IN (SELECT MAX(ROWID) FROM MYTABLE);
-- increment
appid := appid + 1;
-- use these ids to insert with
INSERT INTO MYTABLE (ID, APPLICATION_ID,
, EMPLOYEE_ID
, TITLE_POSITION
, CREATED_DATE
, LAST_MODIFIED
) VALUES(myid, appid,
, EMPLOYEE_ID
, TITLE_POSITION
, CREATED_DATE
, LAST_MODIFIED
);
COMMIT;
-- re-enable the PK constraint
EXECUTE IMMEDIATE 'ALTER TABLE PASS ADD CONSTRAINT MYTABLE_PK PRIMARY KEY (ID)';
COMMIT;
END;
Here's one problem:
SELECT ID
INTO myid
FROM MYTABLE
WHERE ROWID IN (SELECT MAX(ROWID) FROM MYTABLE)
There is no correlation between ID and ROWID, so you're not getting the maximum current ID, you're just getting the one that happens to be on the row that is furthest from the start of a datafile with a high number.
The code you need is:
SELECT COALESCE(MAX(ID),0)
FROM MYTABLE;
Or better yet, just use a sequence.
No idea why you're dropping the PK either.
Furthermore, when you issue the query:
SELECT APPLICATION_ID INTO appid ...
... that could be for a different row than the one you already got the id for, because a change could have been committed to the table.
Of course another issue is that you can't run two instances of this procedure at the same time either.
For David Aldridge, since he wants to look at code instead of the real reason I posted my question, run this ---
CREATE TABLE YOURSCHEMA.TESTING
(
TEST_ID NVARCHAR2(100) NOT NULL
, TEST_TYPE NVARCHAR2(100) NOT NULL
, CONSTRAINT TEST_PK PRIMARY KEY
(
TEST_ID
)
ENABLE
);
create or replace
PROCEDURE PRC_TESTING_INSERT(
TEST_TYPE IN NVARCHAR2
) AS
testid integer := 0;
BEGIN
-- disable PK constraint so it can be updated
EXECUTE IMMEDIATE 'ALTER TABLE TESTING DROP CONSTRAINT TEST_PK';
COMMIT;
-- assign TEST_ID to testid
SELECT TEST_ID INTO testid FROM TESTING WHERE ROWID IN (SELECT MAX(ROWID) FROM TESTING);
-- increment
testid := testid + 1;
-- use this id to insert with
INSERT INTO TESTING (TEST_ID, TEST_TYPE) VALUES(testid, TEST_TYPE);
COMMIT;
-- re-enable the PK constraint
EXECUTE IMMEDIATE 'ALTER TABLE TESTING ADD CONSTRAINT TEST_PK PRIMARY KEY (TEST_ID)';
COMMIT;
END;
SET serveroutput on;
DECLARE
test_type varchar(100);
BEGIN
test_type := 'dude';
YOURSCHEMA.PRC_TESTING_INSERT(test_type);
-- to verify the variable got set and procedure ran, could do:
--dbms_output.enable;
--dbms_output.put_line(test_type);
END;
Now, because there is no data in the table, the stored procedure will fail with ORA-06512: no data found. If you then try and run it again, you will get ORA-02443: cannot drop constraint - nonexistent constraint, because the EXECUTE IMMEDIATE 'ALTER TABLE TESTING DROP CONSTRAINT TEST_PK'; successfully dropped it, and the procedure never ran the command at the end to re-add it. This is what made me think I needed the commits, but even without them, it still will not complete the whole procedure.
To prove that the procedure DOES run, if given proper data, run this after creating the table, but before creating/running the stored procedure:
INSERT INTO TESTING (TEST_ID, TEST_TYPE)
VALUES ('1', 'hi');
And if you run the proc from a new table (not one with its constraint dropped), it will run fine.
Since mathguy didn't post this as the answer, though I'll credit him for the information...
Answer to why I can't see the duplicates is because the COMMIT does not occur in the procedure when it failed due to a datatype mismatch (which we found was actually in the application's code that sent the variable's values into this procedure, not in the stored procedure, itself). (It's also why I'll mark down anyone that says you don't have to add so many COMMIT lines in this procedure.) The commands were run in the session of the user that starts it - in my case, another session of the same DB user I was logged in with, but started from my application, instead of my SQL Developer session. It also explains why I could do a COMMIT, myself, but it did not affect the application's session - I could not commit any actions ran from another session. Had I ran a COMMIT as an OracleCommand and did an .ExecuteNonQuery on my OracleConnection right after the failure within the catch of my application, I would have seen the rows in SQL Developer without having to do a special query.
So, in short, the only way to see the items was with a direct query using WHERE ID =, find the last ID and increment it, and put it in the query.

How to create a trigger which fires when any insert or update is done in a table?

I have created the below trigger, but it is not getting fired after i am doing new insert/update in to the mentioned table:
CREATE OR REPLACE TRIGGER ref_upd_user_phi_details
AFTER
INSERT OR UPDATE --of emp_email_address, ssn_nb
ON ref_adp_employees
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
BEGIN
UPDATE ref_adp_employees
SET emp_email_address = 'QA_' ||emp_email_address,
ssn_nb = nvl2(ssn_nb, NULL, '123-45-6789')
WHERE upper(emp_email_address) NOT LIKE 'QA_%'
AND upper(emp_email_address) LIKE '%#KEENAN.COM';
exception
WHEN others
THEN
NULL;
END;
Can someone please suggest me what i am missing?
As #phonetic_man pointed out, you are hiding any error you get by catching when others and taking no action. Without the exception block you would see that you are causing a mutating table error (ORA-04091), because you are referring to the same table the trigger is against.
If you took out the for each row part to turn it into a statement-level trigger then you would avoid that issue, but now you would have an infinite loop (ORA-00036) - when you try to update the table from within the trigger, that update itself causes the same trigger to fire again; which tries to update the same table yet again, which causes the trigger to fire yet again; etc. until Oracle notices and kills the process.
It would make more sense to use a before-insert row-level trigger to make sure the new values for the row match whatever pattern you are trying to enforce. Maybe something like:
CREATE OR REPLACE TRIGGER ref_upd_user_phi_details
BEFORE INSERT OR UPDATE --of emp_email_address, ssn_nb
ON ref_adp_employees
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
BEGIN
IF upper(:NEW.emp_email_address) NOT LIKE 'QA_%'
AND upper(:NEW.emp_email_address) LIKE '%#KEENAN.COM'
THEN
:NEW.emp_email_address := 'QA_' || :NEW.emp_email_address;
:NEW.ssn_nb := CASE WHEN :NEW.ssn_nb IS NULL THEN '123-45-6789' END;
END IF;
END;
/
And to see what it does:
insert into ref_adp_employees (emp_id, emp_email_address, ssn_nb) values (1, 'TEST_1', '123-45-6789');
insert into ref_adp_employees (emp_id, emp_email_address, ssn_nb) values (2, 'TEST_1#KEENAN.COM', '123-45-9876');
insert into ref_adp_employees (emp_id, emp_email_address, ssn_nb) values (3, 'QA_TEST_1', null);
select emp_id, emp_email_address, ssn_nb from ref_adp_employees;
EMP_ID EMP_EMAIL_ADDRESS SSN_NB
---------- ------------------------------ -----------
1 TEST_1 123-45-6789
2 QA_TEST_1#KEENAN.COM
3 QA_TEST_1
Not sure if you really intended to replace set SSNs with null, and turn nulls into the fixed value; I suspect you are really trying to replace set values with the fixed string and leaves nulls alone, in which case it would be:
:NEW.ssn_nb := CASE WHEN :NEW.ssn_nb IS NOT NULL THEN '123-45-6789' END;
You might also want to move that outside the IF block, so it's done regardless of the email address; I've replicated what your original code was trying to do but that might not be right.
If you have existing data that you want to modify to match these changes, do a one-off update of the whole table - don't try to do that inside a trigger.
Have you heard of Google? There's a myriad of answers and examples out there for triggers, but some things I see at this time:
1) Change trigger to BEFORE insert or update. Use AFTER when making changes to another table, or to run some subsequent process on the table.
2) Take out the comment to the individual fields being changed or added. That was good AFAIK.
3) In your body of the trigger use WHEN INSERTING and WHEN UPDATING. Or if you are just updating the table, change DDL to BEFORE UPDATE only.
4) In the update, reference with set :new.emp_email_address = 'QA_' || :old.emp_email_address ... and so on and so forth. That's where that old as old and new as new becomes important.
Kindly check if the trigger is valid or not by firing the below query..
SELECT *
FROM ALL_OBJECTS
WHERE OBJECT_NAME = trigger_name
AND OBJECT_TYPE = 'TRIGGER'
AND STATUS <> 'VALID'
The trigger is firing after update..
try before update
CREATE OR REPLACE TRIGGER ref_upd_user_phi_details
Before
INSERT OR UPDATE

Writing Procedure to enforce constraints + Testing

I need to set a constraint that the user is unable to enter any records after he/she has entered 5 records in a single month. Would it be advisable that I write a trigger or procedure for that? Else is that any other ways that I can setup the constraint?
Instead of writing a trigger i have opt to write a procedure for the constraint but how do i check if the procedure is working?
Below is the procedure:
CREATE OR REPLACE PROCEDURE InsertReadingCheck
(
newReadNo In Int,
newReadValue In Int,
newReaderID In Int,
newMeterID In Int
)
AS
varRowCount Int;
BEGIN
Select Count(*) INTO varRowCount
From Reading
WHERE ReaderID = newReaderID
AND Trunc(ReadDate,'mm') = Trunc(Sysdate,'mm');
IF (varRowCount >= 5) THEN
BEGIN
DBMS_OUTPUT.PUT_LINE('*************************************************');
DBMS_OUTPUT.PUT_LINE('');
DBMS_OUTPUT.PUT_LINE(' You attempting to enter more than 5 Records ');
DBMS_OUTPUT.PUT_LINE('');
DBMS_OUTPUT.PUT_LINE('*************************************************');
ROLLBACK;
RETURN;
END;
ELSIF (varRowCount < 5) THEN
BEGIN
INSERT INTO Reading
VALUES(seqReadNo.NextVal, sysdate, newReadValue,
newReaderID, newMeterID);
COMMIT;
END;
END IF;
END;
Anyone can help me look through
This is the sort of thing that you should avoid putting in a trigger. Especially the ROLLBACK and the COMMIT. This seems extremely dangerous (and I'm not even sure whether it's possible). You might have other transactions that you wish to commit that you rollback or vice versa.
Also, by putting this in a trigger you are going to get the following error:
ORA-04091: table XXXX is mutating, trigger/function may not see it
There are ways round this but they're excessive and involve doing something funky in order to get round Oracle's insistence that you do the correct thing.
This is the perfect opportunity to use a stored procedure to insert data into your table. You can check the number of current records prior to doing the insert meaning that there is no need to do a ROLLBACK.
It depends upon your application, if insert is already present in your application many times then trigger is better option.
This is a behavior constraint. Its a matter of opinion but I would err on the side of keeping this kind of business logic OUT of your database. I would instead keep track of who added what records in the records table, and on what day/times. You can have a SP to get this information, but then your code behind should handle whether or not the user can see certain links (or functions) based on the data that's returned. Whether that means keeping the user from accessing the page(s) where they insert records, or give them read only views is up to you.
One declarative way you could solve this problem that would obey all concurrency rules is to use a separate table to keep track of number of inserts per user per month:
create table inserts_check (
ReaderID integer not null,
month date not null,
number_of_inserts integer constraint max_number_of_inserts check (number_of_inserts <= 5),
primary key (ReaderID, month)
);
Then create a trigger on the table (or all tables) for which inserts should be capped at 5:
create trigger after insert on <table>
for each row
begin
MERGE INTO inserts_check t
USING (select 5 as ReaderID, trunc(sysdate, 'MM') as month, 1 as number_of_inserts from dual) s
ON (t.ReaderID = s.ReaderID and t.month = s.month)
WHEN MATCHED THEN UPDATE SET t.number_of_inserts = t.number_of_inserts + 1
WHEN NOT MATCHED THEN INSERT (ReaderID, month, number_of_inserts)
VALUES (s.ReaderID, s.month, s.number_of_inserts);
end;
Once the user has made 5 inserts, the constraint max_number_of_inserts will fail.

Resources