ワケあってPLpgSQL + Triggerのクイズを解いたのですが、PLpgSQLの基本はもとより、pg_notify()やらCTEやらJSONやら、実力を試すにはよいクイズだったように思うので、上げておきます。
質問
以下のテーブルがある。
CREATE TABLE transaction ( id SERIAL PRIMARY KEY, payment_id BIGINT, trade_id BIGINT ); CREATE TABLE payment ( id SERIAL PRIMARY KEY, tid BIGINT REFERENCES transaction(id) ON UPDATE RESTRICT ON DELETE RESTRICT NOT NULL, details JSON NOT NULL ); CREATE TABLE trade ( id SERIAL PRIMARY KEY, tid BIGINT REFERENCES transaction(id) ON UPDATE RESTRICT ON DELETE RESTRICT NOT NULL, details JSON NOT NULL ); ALTER TABLE transaction ADD FOREIGN KEY (payment_id) REFERENCES payment(id) ON UPDATE RESTRICT ON DELETE RESTRICT; ALTER TABLE transaction ADD FOREIGN KEY (trade_id) REFERENCES trade(id) ON UPDATE RESTRICT ON DELETE RESTRICT;
次のような操作を行う (中身は本質的ではない)。どちらも自分自身とtransactionテーブルにデータを放り込む。
WITH op(details, tid, pid) AS (VALUES ('{"data":"details"}'::JSON, nextval('transaction_id_seq'), nextval('payment_id_seq')) ), tr AS ( INSERT INTO transaction(id, payment_id) SELECT op.tid, op.pid FROM op RETURNING * ), pm AS ( INSERT INTO payment(id, tid, details) SELECT op.pid, op.tid, op.details FROM op RETURNING * ) SELECT tr AS transaction, pm AS payment FROM tr CROSS JOIN pm; WITH op(details, tid, trid) AS ( VALUES ('{"data":"details"}'::JSON, nextval('transaction_id_seq'), nextval('trade_id_seq')) ), tr AS ( INSERT INTO transaction(id, trade_id) SELECT op.tid, op.trid FROM op RETURNING * ), td AS ( INSERT INTO trade(id, tid, details) SELECT op.trid, op.tid, op.details FROM op RETURNING * ) SELECT tr AS transaction, td AS trade FROM td CROSS JOIN tr;
問題は"テーブルtransactionのトリガを作れ。ただし、tradeとpaymant両テーブルの変更分をpayloadとしてJSON形式でNOTIFYするように。"
答え
私の答え。
CREATE OR REPLACE FUNCTION trans_update () RETURNS trigger AS $$ DECLARE target text; target_id bigint; payload text; query text; BEGIN target := 'payment'; SELECT NEW.payment_id INTO target_id; IF target_id IS NULL THEN target := 'trade'; SELECT NEW.trade_id INTO target_id; END IF; query := 'SELECT to_json(t) FROM ' || target || ' as t WHERE t.id = ' || target_id; EXECUTE query INTO payload; PERFORM pg_notify(target, payload); RETURN NULL; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trans_trig AFTER INSERT ON transaction FOR EACH ROW EXECUTE PROCEDURE trans_update();
実行結果は以下。
sampledb=# LISTEN trade; LISTEN payment; LISTEN LISTEN sampledb=# WITH op(details, tid, trid) AS (VALUES ('{"data":"details"}'::JSON, nextval('transaction_id_seq'), nextval('trade_id_seq'))), tr AS (INSERT INTO transaction(id, trade_id) SELECT op.tid, op.trid FROM op RETURNING *), td AS (INSERT INTO trade(id, tid, details) SELECT op.trid, op.tid, op.details FROM op RETURNING *) SELECT tr AS transaction, td AS trade FROM td CROSS JOIN tr; transaction | trade -------------+---------------------------------- (19,,10) | (10,19,"{""data"":""details""}") (1 row) Asynchronous notification "trade" with payload "{"id":10,"tid":19,"details":{"data":"details"}}" received from server process with PID 25552. sampledb=# WITH op(details, tid, pid) AS (VALUES ('{"data":"details"}'::JSON, nextval('transaction_id_seq'), nextval('payment_id_seq'))), tr AS (INSERT INTO transaction(id, payment_id) SELECT op.tid, op.pid FROM op RETURNING *), pm AS (INSERT INTO payment(id, tid, details) SELECT op.pid, op.tid, op.details FROM op RETURNING *) SELECT tr AS transaction, pm AS payment FROM tr CROSS JOIN pm; transaction | payment -------------+---------------------------------- (20,10,) | (10,20,"{""data"":""details""}") (1 row) Asynchronous notification "payment" with payload "{"id":10,"tid":20,"details":{"data":"details"}}" received from server process with PID 25552.
こんなのいきなり30分から1時間くらいで解けと言われても、しばらくPLpgSQLもトリガも書いてないしNOTIFYも完全に忘れきっていたので1時間では無理だった。リハビリが必要。