ワケあって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時間では無理だった。リハビリが必要。