Damit eine Artikelliste und ihre Anzahl übereinstimmen, führen Sie beide Lesevorgänge in einer REPEATABLE READ READ ONLY-Transaktion auf demselben node-postgres-Client aus. Dieses Tutorial zeigt das Muster und eine Prüfung mit zwei Sitzungen, die Sie auf einem Mac durchführen können.

Warum kann eine Artikelliste von ihrer Anzahl abweichen?

Angenommen, ein Desktop-Skript ruft veröffentlichte Artikel zum Thema craft ab und zählt anschließend die passenden Artikel. Zwischen diesen Lesevorgängen ändert eine andere Sitzung das Thema eines Artikels. Die zurückgegebene Liste kann dann zwei Artikel enthalten, während die Anzahl eins angibt.

PostgreSQLs Standardisolationsstufe READ COMMITTED weist jedem gewöhnlichen SELECT einen Snapshot zu Beginn der jeweiligen Anweisung zu. Aufeinanderfolgende Lesevorgänge können daher selbst innerhalb einer Transaktion unterschiedliche festgeschriebene Daten sehen. PostgreSQL: Transaction Isolation.

Für diese Aufgabe wird eine einheitliche Sicht auf die Artikelzeilen benötigt. Eine spätere Änderung kann beim nächsten Durchlauf erscheinen. PostgreSQLs REPEATABLE READ behält für aufeinanderfolgende Lesevorgänge den Snapshot bei, der mit der ersten Anweisung der Transaktion festgelegt wurde, die weder die Transaktion steuert noch transaktionsbezogene Einstellungen ändert; allein die Ausführung von BEGIN legt diesen Snapshot nicht fest. PostgreSQL: Transaction Isolation.

Die Implementierung muss zwei Anforderungen erfüllen: Legen Sie die Isolationsstufe vor dem Lesen fest, und führen Sie alle zusammengehörigen Abfragen auf demselben Client aus. node-postgres verlangt ausdrücklich, für eine Transaktion einen einzigen Client zu verwenden, und warnt davor, für ihre Anweisungen pool.query() zu verwenden. node-postgres: Transactions.

Bereiten Sie auf Ihrem Mac eine kleine Artikeltabelle vor

Verwenden Sie einen laufenden lokalen PostgreSQL-Server, den Befehl psql und eine unterstützte Node.js-Installation mit npm. Bei den folgenden Befehlen wird angenommen, dass Ihre bestehende lokale Verbindung mit der Datenbank postgres funktioniert. Falls Ihre Arbeitsdatenbank einen anderen Namen hat, ersetzen Sie ihn überall einheitlich. Die Installation von PostgreSQL und die Einrichtung der Verbindung werden vorausgesetzt.

Öffnen Sie Terminal und erstellen Sie einen Projektordner. Das dokumentierte node-postgres-Paket ist pg. node-postgres: Welcome.

mkdir article-snapshot-demo
cd article-snapshot-demo
npm init -y
npm install pg
export PGDATABASE=postgres
psql -d postgres

Geben Sie an der Eingabeaufforderung psql dieses Setup einmal ein. Es erstellt eine eigene Tabelle, die nur synthetische Artikelzeilen enthält. Die expliziten IDs erleichtern auch das Nachvollziehen der späteren Übung.

CREATE TABLE public.article_snapshot_demo (
    id integer PRIMARY KEY,
    title text NOT NULL,
    topic text NOT NULL,
    published boolean NOT NULL
);

INSERT INTO public.article_snapshot_demo
    (id, title, topic, published)
VALUES
    (1, 'Paper Shapes', 'craft', true),
    (2, 'Color Studies', 'craft', true),
    (3, 'Desk Layouts', 'design', true);

Falls die Tabelle bereits existiert, halten Sie an und prüfen Sie das vorherige Fixture, statt es automatisch zu löschen oder zu ersetzen. Die Beispiele gehen von genau diesen drei Ausgangszeilen aus. Stellen Sie psql vor dem Ausführen des Node.js-Skripts auf \q ein.

Lesen Sie Liste und Anzahl über einen ausgecheckten Client

Speichern Sie das folgende vollständige Programm im Projektordner als read-articles.mjs. new Pool() verwendet die dokumentierten PostgreSQL-Verbindungsumgebungsvariablen, einschließlich des oben exportierten Werts PGDATABASE. node-postgres: Connecting.

Das Programm bezieht einen Client, startet die Transaktion mit bereits festgelegten Merkmalen, führt beide Lesevorgänge aus und bestätigt die Transaktion, bevor es das zusammengesetzte Ergebnis zurückgibt. PostgreSQL unterstützt diese Merkmale direkt in BEGIN; die Transaktion endet mit COMMIT oder ROLLBACK. PostgreSQL: BEGIN.

import pg from 'pg';
const { Pool } = pg;

const pool = new Pool({ connectionTimeoutMillis: 5000 });
pool.on('error', (error) => {
  console.error('Idle database client error:', error.message);
  process.exitCode = 1;
});

async function readArticles(topic) {
  const client = await pool.connect();
  let began = false;
  let discardClient = false;

  try {
    await client.query(
      'BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY'
    );
    began = true;

    const articles = await client.query(
      `SELECT id, title, topic
       FROM public.article_snapshot_demo
       WHERE topic = $1 AND published = true
       ORDER BY id`,
      [topic]
    );

    const count = await client.query(
      `SELECT count(*)::text AS total
       FROM public.article_snapshot_demo
       WHERE topic = $1 AND published = true`,
      [topic]
    );

    await client.query('COMMIT');
    began = false;

    return {
      topic,
      articles: articles.rows,
      total: count.rows[0].total
    };
  } catch (error) {
    if (began) {
      try {
        await client.query('ROLLBACK');
      } catch (rollbackError) {
        discardClient = true;
        throw new AggregateError(
          [error, rollbackError],
          'Article read failed; rollback also failed'
        );
      }
    } else {
      discardClient = true;
    }
    throw error;
  } finally {
    client.release(discardClient);
  }
}

try {
  const result = await readArticles('craft');
  console.log(JSON.stringify(result, null, 2));
} catch (error) {
  console.error(error);
  process.exitCode = 1;
} finally {
  await pool.end();
}

Führen Sie das Programm in derselben Terminal-Sitzung aus:

node read-articles.mjs

Beim unveränderten Fixture enthält das erwartete Ergebnis die Artikel 1 und 2, wobei total dem String \"2\" entspricht. Die Anzahl wird ausdrücklich als Text zurückgegeben, daher muss dieses Beispiel sie nicht in eine JavaScript-Zahl umwandeln. Beide Abfragen verwenden dasselbe Themen- und Veröffentlichungsfilter; die Isolation kann nicht übereinstimmende Prädikate korrigieren.

Die Bereinigung gehört zu einem verlässlichen Muster. Bei einem fehlgeschlagenen Lesevorgang wird ein Rollback ausgeführt; schlägt auch dieses fehl, wird der Client verworfen. node-postgres dokumentiert client.release(true) zum Verwerfen eines ausgecheckten Clients und pool.end() zum Herunterfahren des Pools eines Skripts. node-postgres: Pool API.

Dies ist ein eigenständiges Skript und beendet daher nach einem Durchlauf den Pool. In einer bestehenden Anwendung behalten Sie den Lebenszyklus des Anwendungspools bei und ordnen die Transaktionsfunktion darin ein. Jede Hilfsfunktion, die an diesen Lesevorgängen beteiligt ist, muss den ausgecheckten client erhalten; eine Hilfsfunktion, die unbemerkt pool.query() aufruft, überschreitet diese Grenze. node-postgres: Transactions.

Prüfen Sie den Snapshot mit zwei Terminal-Sitzungen

Ein schnelles Skript kann fertig sein, bevor eine andere Sitzung etwas ändert. Führen Sie die folgende bewusst geordnete Übung aus, um den relevanten Zeitpunkt klar festzulegen. Dies sind sitzungsspezifische SQL-Schritte mit der bereits erstellten Tabelle, keine voneinander unabhängigen Setup-Programme. Die erwarteten Datenbankergebnisse entsprechen dem dokumentierten Isolationsverhalten; sie werden nicht als Ergebnis eines tatsächlich ausgeführten PostgreSQL-Tests dargestellt.

Öffnen Sie zwei Terminal-Fenster und führen Sie in jedem psql -d postgres aus. Stellen Sie vor dem Start des Lesers in Sitzung B das Thema des synthetischen Fixtures wieder her. Warten Sie, bis diese Anweisung abgeschlossen ist:

-- Session B: restore the fixture before beginning the exercise.
UPDATE public.article_snapshot_demo
SET topic = 'craft'
WHERE id = 2;

Starten Sie in Sitzung A die Lesertransaktion und rufen Sie die Liste ab. Halten Sie nach Abschluss der Abfrage inne:

-- Session A: run these statements, then pause.
BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY;

SELECT id, title
FROM public.article_snapshot_demo
WHERE topic = 'craft' AND published = true
ORDER BY id;

Die Liste sollte Paper Shapes und Color Studies enthalten. Wechseln Sie nun zu Sitzung B und bestätigen Sie mit dieser eigenständigen Anweisung eine Änderung des Themas. Außerhalb eines expliziten Transaktionsblocks bestätigt PostgreSQL eine erfolgreiche Anweisung automatisch. PostgreSQL: BEGIN.

-- Session B: run while session A remains open.
UPDATE public.article_snapshot_demo
SET topic = 'design'
WHERE id = 2;

Kehren Sie zu Sitzung A zurück und schließen Sie den zusammengehörigen Lesevorgang ab:

-- Session A: finish the original reader transaction.
SELECT count(*)::text AS total
FROM public.article_snapshot_demo
WHERE topic = 'craft' AND published = true;

COMMIT;

Die erwartete Anzahl ist 2 und stimmt mit der früheren Liste von Sitzung A überein. Danach sollte ein neuer Durchlauf des Node.js-Skripts nur Paper Shapes und eine Gesamtzahl von \"1\" zurückgeben. Sowohl das alte als auch das neue Ergebnis stellen jeweils eine einheitliche Sicht dar.

Wiederholen Sie für einen kontrollierten Vergleich die gesamte Übung einschließlich der anfänglichen Wiederherstellung in Sitzung B, starten Sie Sitzung A diesmal aber mit BEGIN ISOLATION LEVEL READ COMMITTED READ ONLY;. Behalten Sie dieselbe Pause und dieselbe Reihenfolge der Änderungen bei. Die erste Liste sollte dann zwei Artikel enthalten, während die spätere Anzahl 1 ist. Bei diesem Vergleich wird nur die Transaktionseinstellung geändert.

Wählen Sie die passende Abgrenzung für zusammengehörige Lesevorgänge

Verwenden Sie diese Entscheidungshilfe, wenn Sie das Beispiel anpassen. Entscheiden Sie zunächst, welche Ausgaben übereinstimmen müssen, und wählen Sie dann den kleinsten Datenbankvorgang, der sie bereitstellt.

Leseraufgabe Geeigneter Ansatz Zu wahrende Bedingung
Eine Artikelliste aus einer einzelnen gewöhnlichen Abfrage zurückgeben Eine Abfrage kann READ COMMITTED verwenden Keine spätere Abfrage muss denselben Snapshot verwenden.
Eine Liste und eine separat abgefragte Anzahl zurückgeben Eine REPEATABLE READ READ ONLY-Transaktion Verwenden Sie einen Client und übereinstimmende Filter.
Mehrere Stapel für ein einheitliches Ergebnis abrufen Die Lesevorgänge innerhalb derselben Transaktion halten Eine neue Transaktion legt eine neue Sicht fest.
Lesevorgänge über mehrere Datenbanksitzungen hinweg koordinieren Snapshot explizit exportieren und importieren Unabhängige Transaktionen teilen nicht automatisch einen Snapshot.

Für mehrere Sitzungen bietet PostgreSQL pg_export_snapshot() und SET TRANSACTION SNAPSHOT. Der Exporter muss geöffnet bleiben, bis die anderen Transaktionen seinen Snapshot importiert haben. Eine importierende Transaktion muss REPEATABLE READ oder SERIALIZABLE verwenden und den Import vor ihrer ersten Abfrage oder Datenänderungsanweisung durchführen. Für den Import bei Serializable gelten zusätzliche Einschränkungen, die in der Befehlsreferenz dokumentiert sind. PostgreSQL: Snapshot Synchronization Functions, PostgreSQL: SET TRANSACTION.

Für diese Desktop-Aufgabe mit Artikelliste und Anzahl vermeidet die Beibehaltung eines einzelnen Clients diesen zusätzlichen Koordinationsaufwand. Schließen Sie die Datenbanklesevorgänge ab und beenden Sie die Transaktion, bevor Sie darauf warten, dass eine Person eine weitere Aktion auswählt.

Welche Probleme löst diese Isolationsstufe nicht?

PostgreSQLs REPEATABLE READ verhindert Phantom-Lesevorgänge, aber Serialisierungsanomalien bleiben möglich. Ein stabiler Snapshot beweist daher nicht, dass gleichzeitige Abläufe so ausgeführt wurden, als wären sie nacheinander erfolgt. Auch Sequenzwerte werden dadurch nicht eingefroren; für Sequenzen gilt ein besonderes Transaktionsverhalten. PostgreSQL: Transaction Isolation.

READ ONLY verbietet Änderungen an gewöhnlichen Tabellen, mit dokumentierten Ausnahmen, etwa bestimmten Vorgängen mit temporären Tabellen. Die Einstellung allein wählt keinen stabilen Snapshot aus. Legen Sie beide Merkmale vor dem Lesen fest: PostgreSQL lässt die Isolationsstufe nach der ersten Abfrage oder Datenänderungsanweisung der Transaktion nicht mehr ändern. PostgreSQL: SET TRANSACTION.

Wenn ein Bericht stärkere Serializable-Semantik erfordert, unterstützt PostgreSQL SERIALIZABLE READ ONLY DEFERRABLE. Beim Erstellen des Snapshots kann es warten; anschließend läuft die Transaktion ohne Abbruch aufgrund eines Serialisierungsfehlers. Dieser Zielkonflikt unterscheidet sich von der hier gezeigten gewöhnlichen Anforderung an das Lesen von Artikeln. PostgreSQL: SET TRANSACTION.

Behalten Sie für die Artikelliste und ihre Anzahl die Abgrenzung der Implementierung klar bei: einen Client beziehen, die Transaktion mit den erforderlichen Merkmalen starten, alle zusammengehörigen Ergebnisse lesen, die Transaktion bestätigen und den Client freigeben. Verwenden Sie dieselben Filter und veröffentlichen Sie das zusammengesetzte Ergebnis erst, nachdem die Transaktion abgeschlossen ist. So steht für die nächste Desktop-Aktion ein einheitlicher Satz Artikeldaten bereit.

Quellen