sql >> Base de Datos >  >> RDS >> PostgreSQL

¿Cómo puedo disparar un activador al final de una cadena de actualizaciones?

En lugar de usar una bandera en report_subscriber en sí mismo, creo que estaría mejor con una cola separada de cambios pendientes. Esto tiene algunos beneficios:

  • Sin disparador de recursividad
  • Debajo del capó, UPDATE es solo DELETE + re-INSERT , por lo que insertarse en una cola en realidad será más barato que voltear una bandera
  • Posiblemente un poco más barato, ya que solo necesita poner en cola los distintos report_id s, en lugar de clonar todo report_subscriber registros, y puede hacerlo en una tabla temporal, por lo que el almacenamiento es contiguo y no es necesario sincronizar nada en el disco
  • No hay condiciones de carrera de las que preocuparse al cambiar las banderas, ya que la cola es local para la transacción actual (en su implementación, los registros afectados por UPDATE report_subscriber no son necesariamente los mismos registros que recogió en el SELECT ...)

Por lo tanto, inicialice la tabla de colas:

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

... poner en cola los cambios a medida que llegan, ignorando todo lo que ya esté en cola:

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

...y procese la cola al final de la sentencia:

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

Hay un pequeño problema con esto:UPDATE no ofrece ninguna garantía sobre el orden de actualización. Esto significa que, si estas dos sentencias se ejecutaran simultáneamente:

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

...entonces existe la posibilidad de un interbloqueo, si intentan actualizar el report registros en orden opuesto. Puede evitar esto aplicando un orden consistente para todas las actualizaciones, pero lamentablemente no hay forma de adjuntar un ORDER BY a una UPDATE declaración; Creo que necesitas recurrir a los cursores:

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

Esto todavía tiene el potencial de interbloquearse si el cliente intenta ejecutar varias declaraciones dentro de la misma transacción (ya que el orden de actualización solo se aplica dentro de cada declaración, pero los bloqueos de actualización se mantienen hasta la confirmación). Puede solucionar esto (más o menos) activando process_pending_changes() solo una vez al final de la transacción (el inconveniente es que, dentro de esa transacción, no verá sus propios cambios reflejados en el report_subscribers matriz).

Aquí hay un esquema genérico para un activador "al confirmar", si cree que vale la pena completarlo:

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