Daripada menggunakan tanda di report_subscriber
sendiri, saya pikir Anda akan lebih baik dengan antrian terpisah dari perubahan yang tertunda. Ini memiliki beberapa manfaat:
- Tidak ada pemicu rekursi
- Di bawah tenda,
UPDATE
hanyaDELETE
+ ulangINSERT
, jadi memasukkan ke dalam antrian sebenarnya akan lebih murah daripada membalik bendera - Mungkin sedikit lebih murah, karena Anda hanya perlu mengantrekan
report_id
yang berbeda s, daripada mengkloning seluruhreport_subscriber
record, dan Anda dapat melakukannya dalam tabel temp, sehingga penyimpanannya berdekatan dan tidak ada yang perlu disinkronkan ke disk - Tidak ada kondisi balapan yang perlu dikhawatirkan saat membalik tanda, karena antreannya bersifat lokal untuk transaksi saat ini (dalam implementasi Anda, catatan dipengaruhi oleh
UPDATE report_subscriber
belum tentu catatan yang sama yang Anda ambil diSELECT
...)
Jadi, inisialisasi tabel antrian:
CREATE FUNCTION create_queue_table() RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
CREATE TEMP TABLE pending_subscriber_changes(report_id INT UNIQUE) ON COMMIT DROP;
RETURN NULL;
END
$$;
CREATE TRIGGER create_queue_table_if_not_exists
BEFORE INSERT OR UPDATE OF report_id, subscriber_name OR DELETE
ON report_subscriber
FOR EACH STATEMENT
WHEN (to_regclass('pending_subscriber_changes') IS NULL)
EXECUTE PROCEDURE create_queue_table();
...mengantrekan perubahan saat mereka tiba, mengabaikan apa pun yang sudah antri:
CREATE FUNCTION queue_subscriber_change() RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
IF TG_OP IN ('DELETE', 'UPDATE') THEN
INSERT INTO pending_subscriber_changes (report_id) VALUES (old.report_id)
ON CONFLICT DO NOTHING;
END IF;
IF TG_OP IN ('INSERT', 'UPDATE') THEN
INSERT INTO pending_subscriber_changes (report_id) VALUES (new.report_id)
ON CONFLICT DO NOTHING;
END IF;
RETURN NULL;
END
$$;
CREATE TRIGGER queue_subscriber_change
AFTER INSERT OR UPDATE OF report_id, subscriber_name OR DELETE
ON report_subscriber
FOR EACH ROW
EXECUTE PROCEDURE queue_subscriber_change();
...dan memproses antrian di akhir pernyataan:
CREATE FUNCTION process_pending_changes() RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
UPDATE report
SET report_subscribers = ARRAY(
SELECT DISTINCT subscriber_name
FROM report_subscriber s
WHERE s.report_id = report.report_id
ORDER BY subscriber_name
)
FROM pending_subscriber_changes c
WHERE report.report_id = c.report_id;
DROP TABLE pending_subscriber_changes;
RETURN NULL;
END
$$;
CREATE TRIGGER process_pending_changes
AFTER INSERT OR UPDATE OF report_id, subscriber_name OR DELETE
ON report_subscriber
FOR EACH STATEMENT
EXECUTE PROCEDURE process_pending_changes();
Ada sedikit masalah dengan ini:UPDATE
tidak menawarkan jaminan apa pun tentang urutan pembaruan. Artinya, jika kedua pernyataan ini dijalankan secara bersamaan:
INSERT INTO report_subscriber (report_id, subscriber_name) VALUES (1, 'a'), (2, 'b');
INSERT INTO report_subscriber (report_id, subscriber_name) VALUES (2, 'x'), (1, 'y');
...maka ada kemungkinan kebuntuan, jika mereka mencoba memperbarui report
catatan dalam urutan yang berlawanan. Anda dapat menghindari ini dengan menerapkan urutan yang konsisten untuk semua pembaruan, tetapi sayangnya tidak ada cara untuk melampirkan ORDER BY
ke UPDATE
penyataan; Saya pikir Anda perlu menggunakan kursor:
CREATE FUNCTION process_pending_changes() RETURNS TRIGGER LANGUAGE plpgsql AS $$
DECLARE
target_report CURSOR FOR
SELECT report_id
FROM report
WHERE report_id IN (TABLE pending_subscriber_changes)
ORDER BY report_id
FOR NO KEY UPDATE;
BEGIN
FOR target_record IN target_report LOOP
UPDATE report
SET report_subscribers = ARRAY(
SELECT DISTINCT subscriber_name
FROM report_subscriber
WHERE report_id = target_record.report_id
ORDER BY subscriber_name
)
WHERE CURRENT OF target_report;
END LOOP;
DROP TABLE pending_subscriber_changes;
RETURN NULL;
END
$$;
Ini masih berpotensi menemui jalan buntu jika klien mencoba menjalankan beberapa pernyataan dalam transaksi yang sama (karena pemesanan pembaruan hanya diterapkan dalam setiap pernyataan, tetapi penguncian pembaruan ditahan hingga komit). Anda dapat mengatasi ini (semacam) dengan menembakkan process_pending_changes()
sekali saja di akhir transaksi (kekurangannya adalah, dalam transaksi itu, Anda tidak akan melihat perubahan Anda sendiri yang tercermin dalam report_subscribers
susunan).
Berikut adalah garis besar umum untuk pemicu "saat komit", jika menurut Anda perlu repot untuk mengisinya:
CREATE FUNCTION run_on_commit() RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
<your code goes here>
RETURN NULL;
END
$$;
CREATE FUNCTION trigger_already_fired() RETURNS BOOLEAN LANGUAGE plpgsql VOLATILE AS $$
DECLARE
already_fired BOOLEAN;
BEGIN
already_fired := NULLIF(current_setting('my_vars.trigger_already_fired', TRUE), '');
IF already_fired IS TRUE THEN
RETURN TRUE;
ELSE
SET LOCAL my_vars.trigger_already_fired = TRUE;
RETURN FALSE;
END IF;
END
$$;
CREATE CONSTRAINT TRIGGER my_trigger
AFTER INSERT OR UPDATE OR DELETE ON my_table
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
WHEN (NOT trigger_already_fired())
EXECUTE PROCEDURE run_on_commit();