sql >> Database teknologi >  >> RDS >> PostgreSQL

Hvordan kan jeg udløse en trigger i slutningen af ​​en kæde af opdateringer?

I stedet for at bruge et flag i report_subscriber selv, tror jeg, du ville være bedre stillet med en separat kø af afventende ændringer. Dette har et par fordele:

  • Ingen triggerrekursion
  • Under emhætten, OPDATERING er bare DELETE + gen-INDSÆT , så det vil faktisk være billigere at indsætte i en kø end at vende et flag
  • Muligvis en del billigere, da du kun behøver at sætte det distinkte report_id i kø s, i stedet for at klone hele report_subscriber poster, og du kan gøre det i en midlertidig tabel, så lageret er sammenhængende, og intet skal synkroniseres til disken
  • Ingen løbsforhold at bekymre sig om, når flagene vendes, da køen er lokal i forhold til den aktuelle transaktion (i din implementering, de poster, der er påvirket af UPDATE report_subscriber er ikke nødvendigvis de samme poster, som du hentede i SELECT ...)

Så initialiser køtabellen:

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

...sæt ændringer i kø, når de ankommer, og ignorer alt, der allerede er i kø:

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

...og behandle køen i slutningen af ​​erklæringen:

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

Der er et lille problem med dette:OPDATERING tilbyder ingen garantier for opdateringsrækkefølgen. Dette betyder, at hvis disse to sætninger blev kørt samtidigt:

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

...så er der en chance for dødvande, hvis de forsøger at opdatere rapporten poster i modsatte rækkefølger. Du kan undgå dette ved at gennemtvinge en ensartet rækkefølge for alle opdateringer, men der er desværre ingen måde at vedhæfte en ORDER BY til en OPDATERING udmelding; Jeg tror, ​​du skal ty til markører:

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

Dette har stadig potentialet til at gå i lås, hvis klienten forsøger at køre flere sætninger inden for den samme transaktion (da opdateringsrækkefølgen kun anvendes inden for hver sætning, men opdateringslåsene holdes indtil commit). Du kan omgå dette (en slags) ved at udløse proces_pending_changes() kun én gang i slutningen af ​​transaktionen (ulempen er, at du inden for denne transaktion ikke vil se dine egne ændringer afspejlet i report_subscribers array).

Her er en generisk oversigt over en "on commit"-udløser, hvis du synes, det er besværet værd at udfylde det:

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. Ugyldig syntaksfejltype=MyISAM i DDL genereret af Hibernate

  2. Kompliceret fodboldliga Dynamisk bestilling i MySQL?

  3. Oprettelse af et tablespace i postgresql

  4. Fejl under opkald til java fra PL/SQL