sql >> Databasteknik >  >> RDS >> PostgreSQL

Hur kan jag aktivera en utlösare i slutet av en kedja av uppdateringar?

Istället för att använda en flagga i report_subscriber själv tror jag att du skulle vara bättre med en separat kö av väntande ändringar. Detta har några fördelar:

  • Ingen utlösarekursion
  • Under huven, UPDATE är bara DELETE + re-INSERT , så att infoga i en kö blir faktiskt billigare än att vända en flagga
  • Möjligen ganska mycket billigare, eftersom du bara behöver ställa det distinkta report_id i kö s, istället för att klona hela report_subscriber poster, och du kan göra det i en temporär tabell, så lagringen är sammanhängande och ingenting behöver synkroniseras till disken
  • Inga tävlingsförhållanden att oroa sig för när du vänder flaggorna, eftersom kön är lokal för den aktuella transaktionen (i din implementering, de poster som påverkas av UPDATE report_subscriber är inte nödvändigtvis samma poster som du plockade upp i SELECT ...)

Så, initiera 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();

...köa upp ändringar när de anländer, ignorera allt som redan finns 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();

...och bearbeta kön i slutet av uttalandet:

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

Det finns ett litet problem med detta:UPDATE erbjuder inga garantier om uppdateringsordern. Detta betyder att om dessa två satser kördes 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');

...då finns det en risk för ett dödläge om de försöker uppdatera report rekord i motsatt ordning. Du kan undvika detta genom att genomdriva en konsekvent ordning för alla uppdateringar, men tyvärr finns det inget sätt att bifoga en ORDER BY till en UPDATE påstående; Jag tror att du måste ta till 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
$$;

Detta har fortfarande potential att låsa sig om klienten försöker köra flera satser inom samma transaktion (eftersom uppdateringsordningen endast tillämpas inom varje sats, men uppdateringslåsen hålls kvar tills commit). Du kan komma runt detta (typ) genom att aktivera process_pending_changes() bara en gång i slutet av transaktionen (nackdelen är att du inom den transaktionen inte kommer att se dina egna ändringar i report_subscribers array).

Här är en allmän översikt för en "på commit"-utlösare, om du tycker att det är värt besväret att fylla i den:

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. Hur man importerar en SQL-fil genom att använda mysqldump på Windows via kommandoraden

  2. Bästa strukturen för en relationsdatabas med artiklar och taggar

  3. varje gång du trycker till heroku visas inte bilder, gem

  4. Hur man hämtar data med nollor i vältalig