PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Comment puis-je déclencher un déclencheur à la fin d'une chaîne de mises à jour ?

Plutôt que d'utiliser un indicateur dans report_subscriber lui-même, je pense que vous seriez mieux avec une file d'attente distincte des modifications en attente. Cela a quelques avantages :

  • Aucune récursivité du déclencheur
  • Sous le capot, UPDATE est juste DELETE + re-INSERT , donc s'insérer dans une file d'attente sera en fait moins cher que de retourner un drapeau
  • Peut-être un peu moins cher, puisque vous n'avez qu'à mettre en file d'attente le report_id distinct s, plutôt que de cloner l'intégralité de report_subscriber enregistre, et vous pouvez le faire dans une table temporaire, de sorte que le stockage est contigu et rien n'a besoin d'être synchronisé sur le disque
  • Aucune condition de concurrence à craindre lors de l'inversion des drapeaux, car la file d'attente est locale à la transaction en cours (dans votre implémentation, les enregistrements affectés par le UPDATE report_subscriber ne sont pas nécessairement les mêmes enregistrements que vous avez sélectionnés dans le SELECT ...)

Donc, initialisez la table de file d'attente :

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

...mettre en file d'attente les modifications au fur et à mesure qu'elles arrivent, en ignorant tout ce qui est déjà en file d'attente :

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

...et traiter la file d'attente à la fin de l'instruction :

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

Il y a un léger problème avec ceci :UPDATE n'offre aucune garantie concernant la commande de mise à jour. Cela signifie que, si ces deux instructions étaient exécutées simultanément :

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

... alors il y a un risque de blocage s'ils tentent de mettre à jour le report enregistrements dans des ordres opposés. Vous pouvez éviter cela en appliquant un ordre cohérent pour toutes les mises à jour, mais malheureusement, il n'y a aucun moyen de joindre un ORDER BY à une UPDATE déclaration; Je pense que vous devez recourir aux curseurs :

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

Cela a toujours le potentiel de se bloquer si le client essaie d'exécuter plusieurs instructions dans la même transaction (car l'ordre de mise à jour n'est appliqué que dans chaque instruction, mais les verrous de mise à jour sont maintenus jusqu'à la validation). Vous pouvez contourner ce (en quelque sorte) en déclenchant process_pending_changes() une seule fois à la fin de la transaction (l'inconvénient est que, dans cette transaction, vous ne verrez pas vos propres modifications reflétées dans le report_subscribers tableau).

Voici un aperçu générique d'un déclencheur "on commit", si vous pensez que cela vaut la peine de le remplir :

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