The problem
I spent today learning about triggers in postgresql - there are triggers "for each statement" and "for each row". I went searching for triggers "for each transaction", and found a few other folks with the same question but no compelling answer.My use case is this - I have 2 tables, a parent (entries) and child (reports), with a one-to-many relationship. I need to add multiple reports for one entry in a single transaction, and then update the associated entry if the transaction succeeds. Here are the table definitions:
create schema transtest; set searchpath to transtest,public; -- it's nice to have a sandbox -- remember to reset searchpath when done create table myents ( id serial primary key, sum integer); -- the entries table create table myrecs ( id serial primary key, entid integer references myents (id), cases integer); -- the reports table, many reports per entry create table myids ( id integer primary key references myents (id)); -- table used to store trigger state
The solution
It appears that deferred constraint triggers are *almost* "per transaction". But constraint triggers are *always* for each row. Yet Postgresql 9.0 allows conditions on triggers - so all I need is to store some state of whether to fire the trigger or not for each row. First, the function that I'll be using in the condition:-- function for when-clause (condition) in trigger -- add id to the "to-do" list if it's not there already -- return True only if it's been added to the least create or replace function f_condition(myid integer) returns boolean as $_$ DECLARE myret boolean := True; newid integer; BEGIN RAISE NOTICE 'Return Value here is %', myret; select count(*) into newid from myids where id = myid; if newid > 0 then -- id is there, return False myret := False; else -- add id to queue, return True insert into myids (id) values (myid); end if; return myret; END; $_$ language plpgsql;
Next, the trigger function, which will clean up after the above function when it's done:
-- trigger function to rebuild myent.sum for this myrecs.id create or replace function f_refresh() returns trigger as $_$ DECLARE mysum integer; myid integer; BEGIN IF (TG_OP = 'DELETE') THEN myid := OLD.entid; ELSE myid := NEW.entid; END IF; RAISE NOTICE 'Return Value here is %', myid; select sum(cases) into mysum from myrecs where entid = myid; -- compute entry sum from records and update it. -- PL/pgSQL rules on queries are quirky. Read the docs. update myents set sum = mysum where id = myid; delete from myids where id = myid; -- clean up RETURN NULL; END; $_$ language plpgsql;
Finally, the trigger definitions using the above.
-- only fire trigger once per myent.id using when clause (condition) -- eval is deferred until transaction concludes create constraint trigger t_cache after insert or update on myrecs initially deferred for each row when ( f_condition(NEW.entid) ) -- comment out for pre pg 9.0 execute procedure f_refresh(); -- need 2, one for each for NEW (insert and update) and OLD (delete) create constraint trigger t_cache_del after delete on myrecs initially deferred for each row when ( f_condition(OLD.entid) ) execute procedure f_refresh();
The test
All software needs to be tester, right?I don't have pg9.0 yet, and it won't hit Ubuntu mainline until Ubuntu Natty, but it looks easy enough to install via a backports repo. So, this isn't fully tested. Comment out the "when" lines in the trigger defines and it will work, albeit running once per row *after* the commit.
In any event, here goes! The sum should be zero until after the commit. If the when clause works correctly, then the trigger should only fire (and emit a notice) once. Likewise, the myids table should be empty.
insert into myents (sum) VALUES ( 0); begin; insert into myrecs (entid, cases) VALUES ( 1, 0), (1, 1), (1,2); select * from myents; -- 0 select * from myids; -- entid from above commit; select * from myents; -- new sum select * from myids; -- empty
No comments:
Post a Comment