How to log old/new rows as XML in triggers - sql

I would like to the the old & new rows as XML to an exceptions table when a trigger cannot succeed. I am used to using a generic EXCEPTION WHEN OTHERS THEN clause to log out failures, what I cannot figure out is have to capture (so I can log) the OLD and NEW pseudorows into XML.
It seems like
old_x := dbms_xmlgen.getxml('select * from OLD');
ought to work, but perhaps I am missing something simple.

You can't select from old, and there is no way to access the old values generically, I'm afraid - you have to specify old.empno, old.ename etc. one by one.
Tom Kyte has shown how to generate triggers to overcome this here on asktom.oracle.com.

Reading the fine document:
A trigger that fires at row level can access the data in the row that it is processing by using correlation names. The default correlation names are OLD, NEW, and PARENT.
OLD, NEW, and PARENT are also called pseudorecords, because they have record structure, but are allowed in fewer contexts than records are. The structure of a pseudorecord is table_name%ROWTYPE, where table_name is the name of the table on which the trigger is created (for OLD and NEW) or the name of the parent table (for PARENT).
(Pseudorecords have also other restrictions explicitly mentioned in the documentation but not quoted here.)
Among all the other things the restrictions mean one can't use pseudorecords e.g. in insert statement extension.
So very much the only thing you can do with a pseudorecord is to refer it's fields one-by-one.
Example
create table a (a number, b date);
create table b (a number, b date);
create or replace trigger a_trg
before insert on a
for each row
begin
-- NEW pseudorecord is not a real record and therefore compilation will fail:
-- PLS-00049: bad bind variable 'NEW'
-- insert into b values :new;
-- NEW pseudorecord fields have to be referenced one-by-one
insert into b(a, b) values(:new.a, :new.b);
end;
/
show errors
insert into a values(1, sysdate);
select * from a;
select * from b;

Related

Accessing old and new values in WHERE clause of CREATE RULE

I'm trying to merge two dump files as outlined in this answer. In working through creating the rule I keep having issues trying to access both the OLD and NEW values of my account table.
The account schema is just a table with a single column of type jsonb called value. Inside the value column are two fields off which I want to create the skip_unique rule. Following is the latest query I've tried and the ensuing response:
CREATE OR REPLACE RULE skip_unique AS ON INSERT TO account WHERE NEW.value->>'opCo' = OLD.value->>'opCo' AND NEW.value->>'customerId' = OLD.value->>'customerId' DO INSTEAD NOTHING;
ERROR: invalid reference to FROM-clause entry for table "old"
LINE 1: ...S ON INSERT TO account WHERE NEW.value->>'opCo' = OLD.value-...
^
HINT: There is an entry for table "old", but it cannot be referenced from this part of the query.
I've tried numerous permutations of the same general query including:
CREATE OR REPLACE RULE skip_unique AS ON INSERT TO account WHERE (EXISTS (SELECT 1 FROM account WHERE NEW.value->>'opCo' = OLD.value->>'opCo' AND NEW.value->>'customerId' = OLD.value->>'customerId')) DO INSTEAD NOTHING;
CREATE OR REPLACE RULE skip_unique AS ON INSERT TO account WHERE (EXISTS (SELECT 1 FROM account AS b WHERE b.value->>'opCo' = account.value->>'opCo' AND b.value->>'customerId' = account.value->>'customerId')) DO INSTEAD NOTHING;
CREATE OR REPLACE RULE skip_unique AS ON INSERT TO account old WHERE (NEW.value->>'opCo' = old.value->>'opCo' AND NEW.value->>'customerId' = old.value->>'customerId') DO INSTEAD NOTHING;
None of them have worked for various reasons.
Thanks in advance for your help!
There is no OLD variable (or rather it's unassigned) on an INSERT trigger. After all, with an INSERT you're inserting something new, so what would you expect OLD to hold?
OLD
Data type RECORD; variable holding the old database row for
UPDATE/DELETE operations in row-level triggers. This variable is
unassigned in statement-level triggers and for INSERT operations.
https://www.postgresql.org/docs/current/static/plpgsql-trigger.html
You should be looking for the value in the table rather than OLD.

Delete a column from a table without changing the environment

I'm working on Oracle SQL database, quite big database. One of (among 150 tables) this table has to be changed because it's redundant (it can be generated through a join). I have been asked to delete a column from this table, to get rid of the redundancy. The problem is that now I have to change code everywhere someone made a insert/update/etc on this table (and don't forget the constraint!). I thought "I can make a view that do the right join" so the problem it's solved for all the select, but it's not working for the insert, because I'm updating 2 tables... Is there a way to solve this problem?
My goal is to rename my original table original_table in original_table_smaller (with one less column) and create a view (or something like a view) called original_table that work like the original table.
Is this possible?
As your view will contain one column that is not present in the real table, you will need to use an instead of trigger to make the view updateable.
Something like this:
create table smaller_table
(
id integer not null primary key,
some_column varchar(20)
);
create view real_table
as
select id,
some_column,
null as old_column
from smaller_table;
Now your old code would run something like this:
insert into real_table
(id, some_column, old_column)
values
(1, 'foo', 'bar');
which results in:
ORA-01733: virtual column not allowed here
To get around this, you need an INSTEAD OF trigger:
create or replace trigger comp_trigger
instead of insert on smaller_table
begin
insert into old_table
(id, some_column)
values
(:new.id, :new.some_column);
end;
/
Now the value for the "old_column" will be ignored. You need something similar for updates as well.
If your view contains a join, then you can handle that situation as well in the trigger. Simply do an update/insert according to the data to two different tables
For more details and examples, see the manual
http://docs.oracle.com/cd/E11882_01/appdev.112/e25519/triggers.htm#i1006376
It is possible to insert/update on views.
You might want to check with USER_UPDATABLE_COLUMNS which columns you can insert to the view.
Check with this query:
select * from user_updatable_columns where table_name = 'VIEW_NAME';
Oracle has two different ways of making views updatable:-
The view is "key preserved" with respect to what you are trying to update. This means the primary key of the underlying table is in the view and the row appears only once in the view. This means Oracle can figure out exactly which underlying table row to update OR
You write an instead of trigger.

multithreading with the trigger

I have written a Trigger which is transferring a record from a table members_new to members_old. The Function of trigger is to insert a record into members_old on after insert in members_new. So suppose a record is getting inserted into a members_new like
nMmbID nMmbName nMmbAdd
1 Abhi Bangalore
This record will get inserted into members_old with the same data structure of the table
My trigger is like :
create trigger add_new_record
after
insert on members_new
for each row
INSERT INTO `test`.`members_old`
(
`nMmbID`,
`nMmbName`,
`nMmbAdd`
)
(
SELECT
`members_new`.`nMmbID`,
`members_new`.`nMmbName`,
`members_new`.`nMmbAdd`
FROM `test`.`members_new`
where nMmbID = (select max(nMmbID) from `test`.`members_new` // written to read the last record from the members_new and stop duplication on the members_old , also this will reduce the chances of any error . )
)
This trigger is working for now , but my confusion is that what will happen if a multiple insertion is happening at one instance of time.
Will it reduce the performance?
Will I face deadlock condition ever in any case as my members_old have FKs?
If any better solution for this situation is there, please give limelight on that
From the manual:
You can refer to columns in the subject table (the table associated with the trigger) by using the aliases OLD and NEW. OLD.col_name refers to a column of an existing row before it is updated or deleted. NEW.col_name refers to the column of a new row to be inserted or an existing row after it is updated.
create trigger add_new_record
after
insert on members_new
for each row
INSERT INTO `test`.`members_old`
SET
`nMmbID` = NEW.nMmbID,
`nMmbName` = NEW.nMmbName,
`nMmbAdd` = NEW.nMmbAdd;
And you will have no problem with deadlocks or whatever. Also it should be much faster, because you don't have to read the max value before (which is also unsecure and might lead to compromised data). Read about isolation levels and transactions if you're interested why...

A trigger to find the sum of one field in a different table and error if it's over a certain value in oracle

I have two tables
moduleprogress which contains fields:
studentid
modulecode
moduleyear
modules which contains fields:
modulecode
credits
I need a trigger to run when the user is attempting to insert or update data in the moduleprogress table.
The trigger needs to:
look at the studentid that the user has input and look at all modules that they have taken in moduleyear "1".
take the modulecode the user input and look at the modules table and find the sum of the credits field for all these modules (each module is worth 10 or 20 credits).
if the value is above 120 (yearly credit limit) then it needs to error; if not, input is ok.
Does this make sense? Is this possible?
#a_horse_with_no_name
This looks like it will work but I will only be using the database to input data manually so it needs to error on input. I'm trying to get a trigger similar to this to solve the problem(trigger doesn't work) and forget that "UOS_" is before everything. Just helps me with my database and other functions.
CREATE OR REPLACE TRIGGER "UOS_TESTINGS"
BEFORE UPDATE OR INSERT ON UOS_MODULE_PROGRESS
REFERENCING NEW AS NEW OLD AS OLD
DECLARE
MODULECREDITS INTEGER;
BEGIN
SELECT
m.UOS_CREDITS,
mp.UOS_MODULE_YEAR,
SUM(m.UOS_CREDITS)
INTO MODULECREDITS
FROM UOS_MODULE_PROGRESS mp JOIN UOS_MODULES m
ON m.UOS_MODULE_CODE = mp.UOS_MODULE_CODE
WHERE mp.UOS_MODULE_YEAR = 1;
IF MODULECREDITS >= 120 THEN
RAISE_APPLICATION_ERROR(-20000, 'Students are only allowed to take upto 120 credits per year');
END IF;
END;
I get the error message :
8 23 PL/SQL: ORA-00947: not enough values
4 1 PL/SQL: SQL Statement ignored
I'm not sure I understand your description, but the way I understand it, this can be solved using a materialized view, which might give better transactional behaviour than the trigger:
CREATE MATERIALIZED VIEW LOG
ON moduleprogress WITH ROWID (modulecode, studentid, moduleyear)
INCLUDING NEW VALUES;
CREATE MATERIALIZED VIEW LOG
ON modules with rowid (modulecode, credits)
INCLUDING NEW VALUES;
CREATE MATERIALIZED VIEW mv_module_credits
REFRESH FAST ON COMMIT WITH ROWID
AS
SELECT pr.studentid,
SUM(m.credits) AS total_credits
FROM moduleprogress pr
JOIN modules m ON pr.modulecode = m.modulecode
WHERE pr.moduleyear = 1
GROUP BY pr.studentid;
ALTER TABLE mv_module_credits
ADD CONSTRAINT check_total_credits CHECK (total_credits <= 120)
But: depending on the size of the table this might however be slower than a pure trigger based solution.
The only drawback of this solution is, that the error will be thrown at commit time, not when the insert happens (because the MV is only refreshed on commit, and the check constraint is evaluated then)

insert data in multiple tables

hi i have a problem to insert data in multiple tables. i have define primary key & reference key in tables now i want to insert data in both tables in single query.......how can i do this...........???????
Your question isn't exactly clear on what the particular problem is. I can see three possibilities:
1. You want to insert into two tables wiht a single INSERT statement
2. You want to do two inserts, but without anything else being able to 'get in the middle'
3. You want to insert into one table, then get the primary key to insert into the second table
The answer to 1. is simple:
You can't.
The answer to 2. is simple too:
BEGIN TRANSACTION
INSERT INTO <table1> (a,b,c) VALUES (1,2,3)
INSERT INTO <table2> (a,b,c) VALUES (1,2,3)
COMMIT TRANSACTION
The answer to 3. is has several possibilities. Each depending on exactly what you want to do. Most likely you want to use SCOPE_IDENTITY() but you may also want to look up ##identity and IDENT_CURRENT() to understand the various different options and complexities.
BEGIN TRANSACTION
INSERT INTO <dimension_table> (name)
VALUES ('my new item')
INSERT INTO <fact_table> (item_id, iteam_value)
VALUES (SCOPE_IDENTITY(), 1)
COMMIT TRANSACTION
This is what transactions are meant for. Standard SQL does not permit a single statement inserting into multiple tables at once. The correct way to do it is:
-- begin transaction
insert into table 1 ...
insert into table 2 ...
commit
Does your language support the INSERT ALL construct? If so, that is the best way to do this. In fact it's the only way. I posted an example of this construct in another SO thread (that example syntax comes from Oracle SQL).
The other option is to build a transactional stored procedure which inserts a record into the primary key table followed by a record into the referencing table.
And 1 of your choice to do that is use ORM (like Hibernate, NHibernate) the you make your object and set other relation to it and finally just save the main object , like:
A a;
B b;
C c;
a.set(b);
a.set(c);
DAO.saveOrUpdate(a);
you must notice your DAO.saveOrUpdate(a); line of code just work with hibernate but it insert data into 3 table A, B, C.