How do I make a trigger to update a column in another table? - postgresql

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.

Related

Function to Automatically set number of corresponding records in one table as default value in second table

I'm kind of stuck here. I'll try to keep it simple.
I have two tables.
Products (product_id, number_of_reviews, ...)
Reviews (main_product_id, review, ...)
main_product_id in Reviews table is the foreign key referencing product_id of Products table.
I need that the number_of_reviews column should automatically update to the number of reviews for that product_id present in the Reviews table. Match can be made by comparing product_id with main_product_id.
I know that I can use count to get number of reviews using this sql statement like:
SELECT COUNT(*) FROM reviews WHERE main_product_id = 'exampleid1'
Here exampleid1 should be product_id from products table.
But how do I create the function that I can call for DEFAULT in column number_of_reviews? Function that automatically takes the product_id from current row and passes it to that select statement and return the number of reviews...
I'm just so stuck here from hours, did a lot of searching but I can't figure it out.
It is my first time asking a question here on stackoverflow and my first time I'm taking interest in coding. PERN stack to be specific.
(I didn't like code for more than 6 years but now finally i built some interest)
First off this is actually a bad plan, you are saving a value the can easily be calculated. However, it seems quite common even though it often leads to complications. The function you need is a trigger; more specifically a trigger function and a trigger on reviews. (see demo)
create or replace function record_a_review_air()
returns trigger
language plpgsql
as $$
begin
update products
set reviews = reviews+1
where prod_id = new.prod_id;
return new;
end;
$$;
create trigger reviews_air
after insert
on reviews
for each row
execute function record_a_review_air();
NOTE: Setting a DEFAULT will not accomplish what you want. Doing so would set the a value when the Product is inserted. But would never be invoked again for that Product.
Here is what worked for me - Thanks to the demo and code provided by #belayer
create or replace function record_a_review_air()
returns trigger
language plpgsql
as $$
begin
update products
set n_reviews = (SELECT COUNT(*) FROM reviews WHERE main_prod_id = prod_id)
where prod_id = coalesce(new.main_prod_id, old.main_prod_id);
return new;
end;
$$;
create trigger reviews_air
after insert or delete or update of rev_id, main_prod_id, review
on reviews
for each row
execute function record_a_review_air();
(I updated the answer to add the new code that works for me on events(insert, update, delete) )

Is INSTEAD OF UPDATE trigger the best option

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.

A trigger fires on a table, but the select on the table returns null. How can I create the code to be able to access the row that fired the trigger?

A trigger fires on a table, but the select on the table returns null. How can I create the code to be able to access the row that fired the trigger?
I have the following in the trigger:
begin
dws_edi_api.init_edi_message(message_id,order_no',supplier_no');
end;
This fires on update of the column row_state in the table out_message_tab
The event fires OK but when in the procedure dws_edi_api.init_edi_message_line I do a select c08 from out_message_tab where message_id = message_id_ (variable from the trigger). it returns null.
I assume the change hasnt been committed. I have tried adding a commit as the first line in my code to force the change to commit but that doesnt help. I have tried adding a dbms_lock.sleep(!0) but that doesnt help either.
I add the code to the procedure in the "show some code box"
procedure init_edi_message_line(message_id in number) is
pragma autonomous_transaction;
message_id_ number;
order_no_ varchar2(20);
supplier_no_ varchar2(20);
c08_ varchar2(200);
cursor c1 is
select c08
from jdifs.out_message_line_tab
where message_id = message_id_
and name = 'HEADER';
begin
-- dbms_lock.sleep(10);
message_id_ := message_id;
open c1;
loop
fetch c1
into c08_;
exit when c08_ is not null;
insert into jdifs.jdws_temp_line_tab
values
(message_id_, '2', c08_, '4');
commit;
END LOOP;
close c1;
EXCEPTION
WHEN NO_DATA_FOUND THEN
-- Do something
null;
WHEN OTHERS THEN
null;
end init_edi_message_line;
EDIT:
Hi, no this didnt solve the problem unfortunately,
I will try again to explain as thourougly as possible.
I have a trigger on the table called out_message_line_tab. When a row is created in that table it contains a big number of columns.
the ones that are interesting to me are message_id(which is a sequential number), order_no (P123456), supplier_no(11242), linenumber(1), part_no (F1524).
When the trigger fires data needs to be fetched from that table (and a table "connected to this table" in this case, out_message_tab.
So the trigger is on out_message_line_tab, but it isnt enough to send the values in the trigger to the procedure, since I need some data from the other table as well.
The primary key between the tables out_message_tab and out_message_line_tab is message_id
So my problem is how to do the select from out_message_tab where message_id = message_id(primary key from out_message_line_tab
When I do, it just says no data found. I assume its because it has not been commited yet.
I hope this is clearer.
Your procedure init_edi_message_line() is defined using pragma autonomous_transaction. That means it executes in a completely separate session. Consequently it cannot see any of the uncommitted data in the session which fired the trigger.
If you want init_edi_message_line() to process data from that session your triggers needs to pass everything to the procedure as an argument. However it's not clear exactly what you're doing - is out_message_line_tab the table which owns the trigger? - so I can't guarantee that it will be easy for you to make the necessary changes.

PostgreSQL How to make an AVG on a column on one table based on an insert on another table

So here's my problem I have this table providers with a column averageRating which is dependent on the rating of a user which is saved on the table reviews
What I'd like is a function that triggers whenever a new review is inserted
So insert review on review table with a provider_id
=> Function that makes the average of the ratings on the provider with the same provider_id
=>
update the averageRating on the provider table
Something along the lines of
CREATE TRIGGER updateAvg
AFTER INSERT OR UPDATE ON reviews
FOR EACH ROW
UPDATE providers.averageRating SET review.rating = (SELECT AVG(rating) FROM reviews)
To answer your question directly, referring to postgres documentation https://www.postgresql.org/docs/9.2/static/plpgsql-trigger.html you must create a function to perform your trigger.
CREATE FUNCTION update_average_rating()
RETURNS void
LANGUAGE plpgsql as $$
BEGIN
UPDATE providers SET averageRating = AVG(rating)
FROM reviews
WHERE providers.id = reviews.provider_id
AND NEW.provider_id = reviews.provider_id;
END;
$$;
CREATE TRIGGER updateAvg
AFTER INSERT OR UPDATE ON reviews
FOR EACH ROW
EXECUTE PROCEDURE update_average_rating();
However, it seems a better solution is use schema design to solve this problem. Wherever you need to display your average rating should be part of a view that takes the average so you can minimize actual cost to your DB performance and retrieve the value on demand as needed.

postgres make a records chain in one table

I have a table in postgres 9.4 and I need to do the following:
When new inserted record comes, I need to find previous records with given parameters and assign it's 'next_message' column value to newly generated record. So I want each record had reference to the next one with given filter, for example 'session_id'. So, if session_id=5, all records with seesion_id=5 should reference next one. I created a trigger that selects previous record and set this field. But it's bad and it will not work in highly loaded db table. How to do that?
That's my trigger:
CREATE OR REPLACE FUNCTION "public"."messages_compute_next_message" () RETURNS trigger
VOLATILE
AS $dbvis$
DECLARE previous_session_message integer;
BEGIN
/*NEW.next_message_id=NEW.id;*/
update message set next_message_id=NEW.id where id=(select max(c.id) from message c where c.session_id=NEW.session_id and c.id<>NEW.id);
RETURN NEW;
END
$dbvis$ LANGUAGE plpgsql
If I post records too frequently, I get many null values in next_message_id fields. And it's logical, otherwise I have to block entire table on every insert. How to do that properly in postgres?
I'd say forget about next_message_id.
The way your trigger looks, it seems to me that messages are ordered by id within one session.
So if you need the previous message for the message with id 42, you can find it with
SELECT max(prev.id)
FROM message prev JOIN message curr
ON prev.session_id = curr.session_id
AND prev.id < curr.id
WHERE curr.id = 42;
Setting the right indexes will speed this up considerably.
I assume there already is an index on id; maybe a second index ON (session_id, id) will help.