sql >> Database >  >> RDS >> PostgreSQL

Hoe kan ik een trigger activeren aan het einde van een reeks updates?

In plaats van een vlag te gebruiken in report_subscriber zelf, denk ik dat je beter af bent met een aparte wachtrij van in behandeling zijnde wijzigingen. Dit heeft een aantal voordelen:

  • Geen trigger-recursie
  • Onder de motorkap, UPDATE is gewoon DELETE + re-INSERT , dus invoegen in een wachtrij is eigenlijk goedkoper dan een vlag omdraaien
  • Misschien een stuk goedkoper, omdat je alleen de duidelijke report_id in de wachtrij hoeft te zetten s, in plaats van de hele report_subscriber te klonen records, en je kunt het in een tijdelijke tabel doen, dus de opslag is aaneengesloten en er hoeft niets naar schijf te worden gesynchroniseerd
  • Geen race-omstandigheden om je zorgen over te maken bij het omdraaien van de vlaggen, omdat de wachtrij lokaal is voor de huidige transactie (in uw implementatie, de records die worden beïnvloed door de UPDATE report_subscriber zijn niet noodzakelijk dezelfde records die u hebt opgehaald in de SELECT ...)

Initialiseer dus de wachtrijtabel:

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

... wijzigingen in de wachtrij plaatsen zodra ze binnenkomen en alles negeren dat al in de wachtrij staat:

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

...en verwerk de wachtrij aan het einde van het statement:

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

Er is een klein probleem hiermee:UPDATE biedt geen garanties over de update-opdracht. Dit betekent dat, als deze twee instructies tegelijkertijd werden uitgevoerd:

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

...dan is er een kans op een impasse, als ze proberen het report bij te werken records in tegenovergestelde volgorde. U kunt dit voorkomen door een consistente volgorde voor alle updates af te dwingen, maar helaas is er geen manier om een ​​ORDER BY toe te voegen naar een UPDATE uitspraak; Ik denk dat je je toevlucht moet nemen tot cursors:

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

Dit heeft nog steeds het potentieel om vast te lopen als de klant meerdere instructies binnen dezelfde transactie probeert uit te voeren (omdat de updatevolgorde alleen binnen elke instructie wordt toegepast, maar de updatevergrendelingen worden vastgehouden tot de commit). Je kunt dit (soort van) omzeilen door process_pending_changes() af te vuren slechts één keer aan het einde van de transactie (het nadeel is dat u binnen die transactie uw eigen wijzigingen niet ziet in de report_subscribers reeks).

Hier is een algemeen overzicht voor een "on commit"-trigger, als je denkt dat het de moeite waard is om deze in te vullen:

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. Aan de slag met Oracle Autonomous Database in the Cloud

  2. Tijdstempel invoegen met JdbcTemplate in Oracle-database (ORA-01858)

  3. Oracle DB Server + APEX + ORDS + JasperReports from scratch (Deel 1)

  4. mysql NULL-waarde in waar in CLAUSE