I am not too familiar with database triggers and/or views. I am currently using PostgreSQL and HSQL; although the database is not too important. I am just wondering if any database offers something like this:
I have an (example) table like this:
CREATE TABLE DUMMY_TABLE (ID INTEGER, NUMBER INTEGER);
I created a view like this:
CREATE VIEW DUMMY_VIEW AS SELECT * FROM DUMMY_TABLE WHERE NUMBER > 5;
I insert a couple of entities:
INSERT INTO DUMMY_TABLE VALUES(1,2);
INSERT INTO DUMMY_TABLE VALUES(1,10);
so of course the DUMMY_VIEW only contains VALUES(1,10) when I call
SELECT * FROM DUMMY_VIEW
So now what I want to do is add a trigger to the DUMMY_VIEW that is called whenever an entity is inserted that has NUMBER > 5.
I have tried adding triggers directly to the DUMMY_VIEW in both HSQL and PostgreSQL; but they say that triggers cannot be added to views.
Is this (or a functionally similar solution) possible?
It should be noted that PostgreSQL 9.1+ supports triggers on views. See WAITING FOR 9.1 – TRIGGERS ON VIEWS for a brief look at this.
Yes, triggers cannot be placed on views directly. What you should do is place a trigger on the base table and check to see if the new NUMBER row has a value greater than 5.
Note: a view is only a stored select statement, so it does not really hold data. That is why one cannot check to see whether data is being inserted, deleted or updated in a view structure.
I think you have to put the trigger on the table, not the view.
The trigger could use a query on the view so that you are DRY.
Is there any other reason the trigger needs to be on the view and not the table?
An example in response to the comment
-- Create function
CREATE FUNCTION doWhatIwant() RETURNS trigger AS '
BEGIN
IF NEW.number > 5 THEN
do_stuff
END IF;
RETURN NEW;
END;
' LANGUAGE plpgsql;
-- Create trigger
CREATE TRIGGER yourTrigger AFTER INSERT ON dummy_table
FOR EACH ROW EXECUTE PROCEDURE doWhatIwant();
I'm not sure what you want to achieve.
A trigger executes code on data change. A view is a (let's say) "callable sub-set of data". It is virtually non-existent, unless you select from it. It can't contain a trigger, because it contains nothing.
So basically you want a trigger on the base table.
This is possible if you add the trigger to the table with the same condition as the view.
The trigger body should have something like:
if (inserted.NUMBER > 5) {
do something;
}
//do nothing if inserted.NUMBER is not > 5
HSQLDB 2.x supports both updatable views and trigger-updatable views.
Your view example is updatable by itself. Therefore you can insert / delete / update rows using the view instead of the table. This will not allow rows containing NUMBER <= 5 in inserts and updates.
You can also define triggers on the view. These triggers are defined with INSTEAD OF INSERT, INSTEAD OF UPDATE or INSTEAD OF DELETE. In the body of the trigger, you can check for the values and either throw an exception for invalid input, or insert the row into the base table.
see http://hsqldb.org/doc/2.0/guide/triggers-chapt.html
Related
I have to check when a table is inserted to/updated to see if a column value exists for the same HotelID and different RoomNo in the same table. I'm thinking that an INSTEAD OF trigger on the table would be a good option, but I read that it's a bad idea to update/insert the table the trigger executes on inside the trigger and you should create the trigger on a view instead (which raises more questions for me)
Is it ok to create a trigger like this? Is there a better option?
CREATE TRIGGER dbo.tgr_tblInterfaceRoomMappingUpsert
ON dbo.tblInterfaceRoomMapping
INSTEAD OF INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #txtRoomNo nvarchar(20)
SELECT #txtRoomNo = Sonifi_RoomNo
FROM dbo.tblInterfaceRoomMapping r
INNER JOIN INSERTED i
ON r.iHotelID = i.iHotelID
AND r.Sonifi_RoomNo = i.Sonifi_RoomNo
AND r.txtRoomNo <> i.txtRoomNo
IF #txtRoomNo IS NULL
BEGIN
-- Insert/update the record
END
ELSE
BEGIN
-- Raise error
END
END
GO
So it sounds like you only want 1 row per combo of HotelID and Sonifi_RoomNo.
CREATE UNIQUE INDEX UQ_dbo_tblInterfaceRoomMapping
ON dbo.tblInterfaceRoomMapping(HotelID,Sonifi_RoomNo)
Now if you try and put a second row with the same values, it will bark at you.
It's (usually) not okay to create a trigger like that.
Your trigger assumes a single row update or insert will only ever occur - is that guaranteed?
What will be the value of #txtRoomNo if multiple rows are inserted or updated in the same batch?
Eg, if an update is performed against the table resulting in 1 row with correct data and 1 row with incorrect data, how do you think your trigger would cope in that situation? Remember triggers fire once per insert/update, not per row.
Depending on your requirments you could keep the instead of trigger concept, however I would suggest a separate trigger for inserts and for updates.
In each you can then insert / update and include a where not exists clause to only allow valid inserts / updates, ignoring inserting or updating anything invalid.
I would avoid raising an error in the trigger, if you need to handle bad data you could also insert into some logging table with the reverse where exists logic and then handle separately.
Ultimately though, it would be best for the application to check if the roomNo is already used.
I am trying to write a trigger which gets data from the table attribute in which multiple rows are inserted corresponding to one actionId at one time and group all that data into the one object:
Table Schema
actionId
key
value
I am firing trigger on rows insertion,SO how can I handle this multiple row insertion and how can I collect all the data.
CREATE TRIGGER attribute_changes
AFTER INSERT
ON attributes
FOR EACH ROW
EXECUTE PROCEDURE log_attribute_changes();
and the function,
CREATE OR REPLACE FUNCTION wflowr222.log_task_extendedattribute_changes()
RETURNS trigger AS
$BODY$
DECLARE
_message json;
_extendedAttributes jsonb;
BEGIN
SELECT json_agg(tmp)
INTO _extendedAttributes
FROM (
-- your subquery goes here, for example:
SELECT attributes.key, attributes.value
FROM attributes
WHERE attributes.actionId=NEW.actionId
) tmp;
_message :=json_build_object('actionId',NEW.actionId,'extendedAttributes',_extendedAttributes);
INSERT INTO wflowr222.irisevents(message)
VALUES(_message );
RETURN NULL;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
and data format is,
actionId key value
2 flag true
2 image http:test.com/image
2 status New
I tried to do it via Insert trigger, but it is firing on each row inserted.
If anyone has any idea about this?
I expect that the problem is that you're using a FOR EACH ROW trigger; what you likely want is a FOR EACH STATEMENT trigger - ie. which only fires once for your multi-line INSERT statement. See the description at https://www.postgresql.org/docs/current/sql-createtrigger.html for a more through explanation.
AFAICT, you will also need to add REFERENCING NEW TABLE AS NEW in this mode to make the NEW reference available to the trigger function. So your CREATE TRIGGER syntax would need to be:
CREATE TRIGGER attribute_changes
AFTER INSERT
ON attributes
REFERENCING NEW TABLE AS NEW
FOR EACH STATEMENT
EXECUTE PROCEDURE log_attribute_changes();
I've read elsewhere that the required REFERENCING NEW TABLE ... syntax is only supported in PostgreSQL 10 and later.
Considering the version of postgres you have, and therefore keeping in mind that you can't use a trigger defined FOR EACH STATEMENT for your purpose, the only alternative I see is
using a trigger after insert in order to collect some information about changes in a utility table
using a unix cron that execute a pl/sql that do the job on data set
For example:
Your utility table
CREATE TABLE utility (
actionid integer,
createtime timestamp
);
You can define a trigger FOR EACH ROW with a body that do something like this
INSERT INTO utilty values(NEW.actionid, curent_timestamp);
And, finally, have a crontab UNIX that execute a file or a procedure that to something like this:
SELECT a.* FROM utility u JOIN yourtable a ON a.actionid = u.actionid WHERE u.createtime < current_timestamp;
// do something here with records selected above
TRUNCATE table utility;
If you had postgres 9.5 you could have used pg_cron instead of unix cron...
I have researched quite a bit but couldn't find what I wanted-
(I have shallow knowledge on TRIGGERS in SQL- pardon me!)
Qn: I have all the THREE Triggers on my table (Insert, Update & Delete)
In my AFTER INSERT Trigger: I need to "update" the "inserted" column
and I was using :
UPDATE Table_name
SET Column_name = #Input
(currently)
But I was requested to use something like:
UPDATE "Inserted.column_name"
SET Column_name = #Input
But this generally cannot happen as it throws me an error:
The logical tables INSERTED and DELETED cannot be updated
Can someone help me out please?
I have seen posts on using INSTEAD OF TRIGGER but that doesn't serve my purpose.. Thanks in advance! Appreciate your help!
You need to update the actual, underlying table - not the Inserted pseudo table....
You need to join the tables on the primary key, and then update your actual data table - something like
CREATE TRIGGER trg_Insert_Sample
ON dbo.YourTableName
AFTER INSERT
AS
UPDATE dbo.YourTableName
SET SomeColumn = i.SomeValue
FROM Inserted i
WHERE dbo.YourTableName.PrimaryKey = i.PrimaryKey
or something along those lines....
You also need to be aware that the trigger is called once per statement - not once per row - so if your INSERT statements inserts 10 rows at once (from e.g. a SELECT), your trigger is called once, and Inserted will contain 10 rows - so you need to make sure your trigger code is capable of handling this situation and is written in a proper, set-based manner (no SELECT #Value = SomeColumn FROM Inserted - that won't work!)
So I am working on adding a last updated time to the database for my app's server. The idea is that it will record the time an update is applied to one of our trips and then the app can send a get request to figure out if it's got all of the correct up to date information.
I've added the column to our table, and provided the service for it all, and finally manage to get a trigger going to update the column every time a change is made to a trip in it's trip table. My problem now comes from the fact that the information that pertains to a trip is stored across a multitude of other tables as well (for instance, there are tables for the routes that make up a trip and the photos that a user can see on the trip, etc...) and if any of that data changes, then the trip's update time also needs to change. I can't for the life of me figure out how to set up the trigger so that when I change some route information, the last updated time for the trip(s) the route belongs to will be updated in it's table.
This is my trigger code as it stands now: it updates the trip table's last updated column when that trip's row is updated.
CREATE OR REPLACE FUNCTION record_update_time() RETURNS TRIGGER AS
$$
BEGIN
NEW.last_updated=now();
RETURN NEW;
END;
$$
LANGUAGE PLPGSQL;
CREATE TRIGGER update_entry_on_entry_change
BEFORE UPDATE ON mydatabase.trip FOR EACH ROW
EXECUTE PROCEDURE record_update_time();
--I used the next two queries just to test that the trigger works. It
--probably doesn't make a difference to you but I'll keep it here for reference
UPDATE mydatabase.trip
SET title='Sample New Title'
WHERE id = 2;
SELECT *
FROM mydatabase.trip
WHERE mydatabase.trip.id < 5;
Now I need it to update when the rows referencing the trip row with a foreign key get updated. Any ideas from someone more experienced with SQL triggers than I?
"mydatabase" is a remarkably unfortunate name for a schema.
The trigger function could look like this:
CREATE OR REPLACE FUNCTION trg_upaft_upd_trip()
RETURNS trigger
LANGUAGE plpgsql AS
$func$
BEGIN
UPDATE mydatabase.trip t -- "mydatabase" = schema name (?!)
SET last_updated = now()
WHERE t.id = NEW.trip_id -- guessing column names
RETURN NULL; -- calling this AFTER UPDATE
END
$func$;
And needs to be used in a trigger on every related table (not on trip itself):
CREATE TRIGGER upaft_upd_trip
AFTER UPDATE ON mydatabase.trip_detail
FOR EACH ROW EXECUTE PROCEDURE trg_upaft_upd_trip();
You also need to cover INSERT and DELETE (and possibly COPY) on all sub-tables ...
This approach has many potential points of failure. As alternative, consider a query or view that computes the latest last_updated from sub-tables dynamically. If you update often this might be the superior approach.
If you rarely UPDATE and SELECT often, your first approach might pay.
I have a trigger function for a table test which has the following code snippet:
IF TG_OP='UPDATE' THEN
IF OLD.locked > 0 AND
( OLD.org_id <> NEW.org_id OR
OLD.document_code <> NEW.document_code OR
-- other columns ...
)
THEN
RAISE EXCEPTION 'Message';
-- more code
So I am statically checking all the column's new value with its previous value to ensure integrity. Now every time my business logic changes and I have to add new columns into that table, I will have to modify this trigger each time. I thought it would be better if somehow I could dynamically check all the columns of that table, without explicitly typing their name.
How can it be done?
From 9.0 beta2 documentation about WHEN clause in triggers, which might be able to be used in earlier versions within the trigger body:
OLD.* IS DISTINCT FROM NEW.*
or possibly (from 8.2 release notes)
IF row(new.*) IS DISTINCT FROM row(old.*)
Take a look at the information_schema, there is a view "columns". Execute a query to get all current columnnames from the table that fired the trigger:
SELECT
column_name
FROM
information_schema.columns
WHERE
table_schema = TG_TABLE_SCHEMA
AND
table_name = TG_TABLE_NAME;
Loop through the result and there you go!
More information can be found in the fine manual.
In Postgres 9.0 or later add a WHEN clause to your trigger definition (CREATE TRIGGER statement):
CREATE TRIGGER foo
BEFORE UPDATE
FOR EACH ROW
WHEN (OLD IS DISTINCT FROM NEW) -- parentheses required!
EXECUTE PROCEDURE ...;
Only possible for triggers BEFORE / AFTER UPDATE, where both OLD and NEW are defined. You'd get an exception trying to use this WHEN clause with INSERT or DELETE triggers.
And radically simplify the trigger function accordingly:
...
IF OLD.locked > 0 THEN
RAISE EXCEPTION 'Message';
END IF;
...
No need to test IF TG_OP='UPDATE' ... since this trigger only works for UPDATE anyway.
Or move that condition in the WHEN clause, too:
CREATE TRIGGER foo
BEFORE UPDATE
FOR EACH ROW
WHEN (OLD.locked > 0
AND OLD IS DISTINCT FROM NEW)
EXECUTE PROCEDURE ...;
Leaving only an unconditional RAISE EXCEPTION in your trigger function, which is only called when needed to begin with.
Read the fine print:
In a BEFORE trigger, the WHEN condition is evaluated just before the
function is or would be executed, so using WHEN is not materially
different from testing the same condition at the beginning of the
trigger function. Note in particular that the NEW row seen by the
condition is the current value, as possibly modified by earlier
triggers. Also, a BEFORE trigger's WHEN condition is not allowed to
examine the system columns of the NEW row (such as oid), because those
won't have been set yet.
In an AFTER trigger, the WHEN condition is evaluated just after the
row update occurs, and it determines whether an event is queued to
fire the trigger at the end of statement. So when an AFTER trigger's
WHEN condition does not return true, it is not necessary to queue an
event nor to re-fetch the row at end of statement. This can result in
significant speedups in statements that modify many rows, if the
trigger only needs to be fired for a few of the rows.
Related:
Fire trigger on update of columnA or ColumnB or ColumnC
To also address the question title
Is it possible to dynamically loop through a table's columns?
Yes. Examples:
Handle result when dynamic SQL is in a loop
Removing all columns with given name
Iteration over RECORD variable inside trigger
Use pl/perl or pl/python. They are much better suited for such tasks. much better.
You can also install hstore-new, and use it's row->hstore semantics, but that's definitely not a good idea when using normal datatypes.