PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

Jak mogę uruchomić wyzwalacz na końcu łańcucha aktualizacji?

Zamiast używać flagi w report_subscriber myślę, że lepiej byłoby mieć osobną kolejkę oczekujących zmian. Ma to kilka zalet:

  • Brak rekurencji wyzwalania
  • Pod maską UPDATE to po prostu DELETE + ponownie INSERT , więc wstawienie do kolejki będzie tańsze niż przerzucenie flagi
  • Prawdopodobnie trochę taniej, ponieważ wystarczy umieścić w kolejce odrębny report_id zamiast klonować cały report_subscriber rekordy i możesz to zrobić w tabeli tymczasowej, więc pamięć jest ciągła i nic nie musi być synchronizowane z dyskiem
  • Brak sytuacji wyścigu, o które trzeba się martwić podczas odwracania flag, ponieważ kolejka jest lokalna dla bieżącej transakcji (w twojej implementacji rekordy, na które ma wpływ UPDATE report_subscriber niekoniecznie są tymi samymi rekordami, które wybrałeś w SELECT ...)

Więc zainicjuj tabelę kolejek:

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();

...kolejkuj zmiany, gdy się pojawią, ignorując wszystko, co już znajduje się w kolejce:

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();

...i przetwórz kolejkę na końcu instrukcji:

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();

Jest z tym niewielki problem:UPDATE nie oferuje żadnych gwarancji co do kolejności aktualizacji. Oznacza to, że jeśli te dwie instrukcje zostały uruchomione jednocześnie:

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');

...wtedy istnieje szansa na zakleszczenie, jeśli spróbują zaktualizować report rekordy w przeciwnej kolejności. Możesz tego uniknąć, wymuszając spójne porządkowanie wszystkich aktualizacji, ale niestety nie ma możliwości dołączenia ORDER BY do UPDATE oświadczenie; Myślę, że musisz uciekać się do kursorów:

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
$$;

To nadal może spowodować zakleszczenie, jeśli klient spróbuje uruchomić wiele instrukcji w ramach tej samej transakcji (ponieważ kolejność aktualizacji jest stosowana tylko w ramach każdej instrukcji, ale blokady aktualizacji są utrzymywane do momentu zatwierdzenia). Możesz to obejść (w pewnym sensie), uruchamiając process_pending_changes() tylko raz na koniec transakcji (wadą jest to, że w ramach tej transakcji nie zobaczysz własnych zmian odzwierciedlonych w report_subscribers tablica).

Oto ogólny zarys wyzwalacza „po zatwierdzeniu”, jeśli uważasz, że warto go wypełnić:

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();



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Wybieranie pasującego podzbioru w relacji wiele do wielu

  2. Wdrażasz bazę danych Postgres na platformie Azure Container Instance?

  3. Jak wstrzymać wykonywanie instrukcji w PostgreSQL

  4. Nieoczekiwany wpływ filtrowania na wynik zapytania crosstab()

  5. Postgres odpowiednik CROSS APPLY w SQL Server