Damit eine node-postgres-Transaktion zusammenbleibt, leihen Sie mit pool.connect() einen Client aus, führen Sie jede Anweisung über diesen Client aus und geben Sie ihn nach dem Commit oder Rollback wieder frei. Übergeben Sie diesen Client auch an Hilfsfunktionen. Der offizielle Leitfaden zu Transaktionen schreibt dieses Muster vor.
Dieses Tutorial richtet sich an Entwickler, die mit Terminal auf einem Mac mehrere Änderungen an Artikelzeilen gemeinsam erfolgreich ausführen oder zurücksetzen möchten. Es enthält ein vollständiges Programm mit synthetischen Artikelzeilen, einen gezielten Rollback-Test und eine Diagnosesequenz für Abfragen, die in Hilfsfunktionen verborgen sind.
Warum unterbricht pool.query() den Transaktionsumfang?
Ein Pool verwaltet Verbindungen; er sorgt nicht dafür, dass jede Abfrage Teil derselben Transaktion ist. Jeder Aufruf von pool.query() leiht intern einen verfügbaren Client aus. Aufeinanderfolgende Aufrufe verwenden daher nicht garantiert dieselbe Verbindung. Die node-postgres-Pool-API warnt ausdrücklich davor, diese Methode für Transaktionen zu verwenden.
Wenn Sie beispielsweise BEGIN über den Pool senden und anschließend einen Insert über den Pool ausführen, befindet sich dieser Insert nicht zuverlässig innerhalb der gestarteten Transaktion. PostgreSQL führt Anweisungen außerhalb eines expliziten Transaktionsblocks normalerweise in eigenen Transaktionen aus. Ein späterer Rollback auf einer anderen Verbindung kann diese unabhängig committeten Änderungen nicht rückgängig machen. Siehe die BEGIN-Dokumentation von PostgreSQL.
Die entscheidende Grenze ist das ausgecheckte Client-Objekt. Der Name einer Hilfsfunktion, das Modul, in dem sie enthalten ist, und die Tatsache, dass der Aufrufer auf sie wartet, legen nicht fest, ob sie zur Transaktion gehört. Eine Hilfsfunktion nimmt an der Transaktion teil, wenn ihre Abfragen den Client verwenden, der die Transaktion gestartet hat. Das folgt unmittelbar aus der Anforderung, denselben Client zu verwenden, die im Leitfaden zu Transaktionen beschrieben ist.
Führen Sie ein vollständiges Commit-und-Rollback-Beispiel auf Ihrem Mac aus
Sie benötigen Node.js mit npm, einen laufenden PostgreSQL-Server und eine vorhandene lokale Entwicklungsdatenbank. In diesem Ablauf wird angenommen, dass die Datenbank article_demo heißt und Ihre üblichen lokalen Verbindungseinstellungen bereits funktionieren. Die Installation der Datenbank ist nicht Teil dieser Transaktionsaufgabe.
Erstellen Sie im Terminal ein neues Arbeitsverzeichnis und installieren Sie den Treiber:
mkdir pg-transaction-demo
cd pg-transaction-demo
npm init -y
npm install pg
Speichern Sie das folgende vollständige Programm unter transaction-demo.mjs. Seine temporäre Tabelle enthält ausschließlich synthetische Artikel-IDs und Titel. Temporäre Tabellen in PostgreSQL überstehen normalerweise Commits und werden beim Ende der Sitzung entfernt. So kann das Programm einen erfolgreichen Batch prüfen, nachdem ein zweiter Batch zurückgesetzt wurde. Siehe CREATE TABLE.
Die Transaktionshilfsfunktion unten folgt der dokumentierten Sequenz BEGIN, COMMIT und ROLLBACK. Sie behandelt außerdem explizit den Fall, dass auch der Rollback fehlschlägt, und bewahrt beide Fehler auf, statt den ursprünglichen Fehler zu ersetzen. Abfragewerte verwenden die unter Abfragen dokumentierte Text-und-Werte-API.
import pg from 'pg';
import assert from 'node:assert/strict';
const { Pool } = pg;
async function inTransaction(client, work) {
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
try {
const result = await work(client);
await client.query('COMMIT');
return result;
} catch (error) {
try {
await client.query('ROLLBACK');
} catch (rollbackError) {
throw new AggregateError(
[error, rollbackError],
'Transaction failed; rollback also failed'
);
}
throw error;
}
}
async function insertArticle(client, article) {
const result = await client.query(
`INSERT INTO pg_temp.transaction_demo_articles (id, title)
VALUES ($1, $2)
RETURNING id, title`,
[article.id, article.title]
);
return result.rows[0];
}
async function insertBatch(client, articles) {
const inserted = [];
for (const article of articles) {
inserted.push(await insertArticle(client, article));
}
return inserted;
}
async function main() {
const pool = new Pool({ connectionTimeoutMillis: 5000 });
let client;
let discardClient = false;
pool.on('error', (error) => {
console.error('Idle client error:', error);
process.exitCode = 1;
});
try {
client = await pool.connect();
await client.query(`
CREATE TEMP TABLE transaction_demo_articles (
id text PRIMARY KEY,
title text NOT NULL
) ON COMMIT PRESERVE ROWS
`);
const saved = await inTransaction(client, (tx) =>
insertBatch(tx, [
{ id: 'article-001', title: 'A guide to paper shapes' },
{ id: 'article-002', title: 'Choosing a notebook layout' }
])
);
assert.equal(saved.length, 2);
await assert.rejects(
() => inTransaction(client, (tx) =>
insertBatch(tx, [
{ id: 'article-003', title: 'Arranging a reading desk' },
{ id: 'article-001', title: 'Deliberate duplicate ID' }
])
),
(error) => error.code === '23505'
);
const remaining = await client.query(`
SELECT id, title
FROM pg_temp.transaction_demo_articles
ORDER BY id
`);
assert.deepEqual(
remaining.rows.map((row) => row.id),
['article-001', 'article-002']
);
console.log('Commit and rollback checks passed.');
console.table(remaining.rows);
} catch (error) {
discardClient = true;
throw error;
} finally {
if (client) client.release(discardClient);
await pool.end();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Führen Sie das Programm im selben Verzeichnis aus:
PGDATABASE=article_demo node transaction-demo.mjs
Der Pool liest PostgreSQL-Verbindungsvariablen aus der Umgebung. Wenn Ihre lokale Einrichtung andere Verbindungseinstellungen erfordert, geben Sie zusätzlich zu PGDATABASE die passenden Werte für PGHOST und PGPORT an. Der offizielle Leitfaden zum Herstellen einer Verbindung beschreibt diese Variablen und ihre Standardwerte.
Bei erfolgreicher Ausführung erwarten Sie Commit and rollback checks passed. gefolgt von den beiden ursprünglichen Artikelzeilen. Der zweite Batch fügt article-003 ein und versucht anschließend, article-001 erneut zu verwenden. Dieses Duplikat erzeugt SQLSTATE 23505, das in Behandlung von Serialisierungsfehlern als unique_violation dokumentiert ist. Durch das Zurücksetzen des Batches wird auch der zuvor eingefügte Datensatz entfernt; die Assertion prüft, dass article-003 nicht vorhanden ist.
Die Erfolgspfade der Transaktionshilfsfunktion sowie die Fehlerpfade bei Arbeits- und Rollback-Fehlern wurden mit einem deterministischen Mock-Client geprüft. Das Datenbankbeispiel wurde hier nicht ausgeführt, weil in der Umgebung das Paket pg und eine PostgreSQL-Testverbindung fehlten; die obige Ausgabe beschreibt das erwartete Ergebnis.
Wie sollten Hilfsfunktionen den Transaktionsclient gemeinsam verwenden?
Verwenden Sie in der gesamten Aufrufkette explizite Client-Parameter. Im Programm leiht main() den Client aus, inTransaction() übergibt dasselbe Objekt an seinen Callback und insertBatch() reicht es an insertArticle() weiter. In keiner der beiden Insert-Hilfsfunktionen wird eine Verbindung ausgecheckt.
Achten Sie bei dieser Implementierung auf klare Zuständigkeiten:
| Funktion | Zuständigkeit |
|---|---|
main() |
Den Client ausleihen, die Demo vorbereiten, den Client freigeben und den Pool des Skripts schließen. |
inTransaction() |
Eine Transaktion auf dem übergebenen Client starten und abschließen. |
insertBatch() |
Jeden Artikelvorgang abwarten und den übergebenen Client weiterreichen. |
insertArticle() |
Den Insert über den übergebenen Client ausführen. |
Der Vertrag der Hilfsfunktion ist bewusst eng gefasst: Rufen Sie inTransaction() nur auf, wenn auf diesem Client keine Transaktion aktiv ist, und lassen Sie Fehler aus dem Callback nach außen gelangen. Ein Aufruf von BEGIN innerhalb einer bestehenden PostgreSQL-Transaktion erzeugt keine unabhängige verschachtelte Transaktion; PostgreSQL dokumentiert Savepoints als Mechanismus für verschachtelte Arbeit. Siehe BEGIN.
Warten Sie jede zur Transaktion gehörende Operation ab, bevor Sie aus ihrem Callback zurückkehren. In diesem Beispiel macht die sequenzielle Schleife den Abschlusszeitpunkt eindeutig. Werden asynchrone Inserts gestartet, ohne ihre Promises zurückzugeben oder abzuwarten, kann der Callback beendet werden, bevor alle Ergebnisse vorliegen.
Was sollte passieren, wenn eine Abfrage oder ein Rollback fehlschlägt?
Ein gewöhnlicher Abfragefehler gelangt zum Catch-Block der Transaktionshilfsfunktion. Dieser versucht, auf demselben Client einen Rollback auszuführen, und löst den ursprünglichen Fehler erneut aus. Das gezielte Duplikat wird erst akzeptiert, nachdem der Rollback abgeschlossen ist. Schlägt der Rollback fehl, löst die Hilfsfunktion einen AggregateError aus; die Prüfung auf den Duplikatfehler akzeptiert ihn nicht, und die Ausführung gelangt zum äußeren Fehler-Handler.
Das Beispiel zerstört den Client bei jedem unerwarteten Fehler vorsichtshalber mit client.release(true). Im normalen Ablauf gibt es den Client mit client.release(false) frei. Das Argument zum Zerstören ist in der Pool-API dokumentiert.
Geben Sie einen erfolgreich ausgecheckten Client immer wieder frei. Andernfalls müssen spätere Aufrufer möglicherweise auf eine Verbindung warten, die nie wieder verfügbar wird. Schließen Sie den Pool, nachdem dieses eigenständige Skript beendet ist; in einer Anwendung gehört das Herunterfahren des Pools zum Herunterfahren der Anwendung. Der Pooling-Leitfaden erläutert die Freigabeanforderungen und wie pool.end() darauf wartet, dass ausgecheckte Clients zurückgegeben werden.
Garantiert ein einzelner Client einen stabilen Snapshot?
Die Kontinuität des Clients und die Isolationsstufe beantworten unterschiedliche Fragen. Der Client legt fest, welche Transaktion Ihre Anweisungen erhält. Die Isolationsstufe legt fest, was diese Anweisungen sehen können, während andere Transaktionen laufen. Die Dokumentation zur Transaktionsisolation von PostgreSQL definiert diese Grenzen:
| Isolationsstufe | Relevante Einschränkung |
|---|---|
READ COMMITTED |
Die Standardeinstellung. Aufeinanderfolgende gewöhnliche Lesevorgänge können verschiedene gleichzeitig committete Daten sehen. Jede Anweisung erhält einen neuen Snapshot. |
REPEATABLE READ |
Verwendet einen Snapshot, der mit der ersten Anweisung festgelegt wird, die keine Transaktionssteuerung ist. PostgreSQL verhindert Phantom Reads, aber Serialisierungsanomalien sind weiterhin möglich. |
SERIALIZABLE |
Committete serialisierbare Transaktionen verhalten sich so, als wären sie in einer bestimmten Reihenfolge ausgeführt worden; eine Transaktion kann fehlschlagen und muss dann wiederholt werden. |
READ UNCOMMITTED |
Verhält sich in PostgreSQL wie READ COMMITTED. |
Lesevorgänge können außerdem Änderungen sehen, die zuvor in derselben Transaktion vorgenommen wurden. Wenn Sie also Ihren eigenen Insert sehen, bedeutet das nicht, dass alle späteren Lesevorgänge eine unveränderliche Sicht auf Änderungen aus anderen Transaktionen haben. Siehe Transaktionsisolation.
Das Beispiel wählt in seiner ersten Anweisung ausdrücklich READ COMMITTED. Um eine andere Stufe zu verwenden, ändern Sie diese Anweisung, bevor Sie die Transaktion ausführen. PostgreSQL erlaubt Transaktionseigenschaften in BEGIN. Wenn Sie stattdessen SET TRANSACTION verwenden, muss die Isolationsstufe vor der ersten Abfrage oder Datenänderungsanweisung festgelegt werden. Siehe BEGIN und SET TRANSACTION.
Wenn Sie REPEATABLE READ oder SERIALIZABLE wählen, sollten Sie auf SQLSTATE 40001 vorbereitet sein. Wiederholen Sie die vollständige Transaktion einschließlich der Logik, die Anweisungen und Werte auswählt, statt nur die fehlgeschlagene Abfrage erneut auszuführen. Das Beispiel gibt solche Fehler absichtlich weiter, statt Wiederholungsversuche zu implementieren. Die Behandlung von Serialisierungsfehlern von PostgreSQL erläutert diese Anforderung und weist darauf hin, dass auch Wiederholungsversuche erneut fehlschlagen können.
Wie finden Sie eine Abfrage, die aus der Transaktion herausgerutscht ist?
Verfolgen Sie einen vorgesehenen Batch vom Auschecken der Verbindung bis zu seinem abschließenden Commit oder Rollback. Verwenden Sie diese Schritte zur Überprüfung Ihres Codes:
- Suchen Sie den Aufruf von
pool.connect()und ermitteln Sie, welchen Client er zurückgibt. - Verfolgen Sie alle Hilfsfunktionen, die zwischen
BEGINund dem Abschluss der Transaktion aufgerufen werden. Prüfen Sie, ob jede diesen Client erhält und verwendet. - Untersuchen Sie gemeinsam genutzte Abfrage-Wrapper auf verborgene Aufrufe von
pool.query()oder zusätzliche Verbindungsausleihen. - Vergewissern Sie sich, dass jede Operation abgewartet wird und Hilfsfunktionen Fehler weitergeben.
- Passen Sie den synthetischen Test mit dem doppelten Datensatz an Ihren Batch an und prüfen Sie, ob ein früherer Insert verschwindet, nachdem eine spätere Anweisung fehlschlägt.
Ein erfolgreicher Rollback-Test liefert nützliche Hinweise für den geprüften Pfad, belegt aber nicht, dass jeder Anwendungszweig den richtigen Client verwendet. Prüfen Sie auch bedingte Hilfsfunktionen. Wenn Abfragen auf einem Client bleiben, sich wiederholte Lesevorgänge aber trotzdem unterscheiden, vergleichen Sie dieses Verhalten mit der gewählten Isolationsstufe, bevor Sie die Verbindungsverwaltung ändern.
Halten Sie Ausleihe und Freigabe sichtbar, übergeben Sie den Client explizit und sorgen Sie dafür, dass die Transaktion erst abgeschlossen wird, wenn alle vorgesehenen Vorgänge beendet sind. So erhalten Sie eine konkrete Struktur zur Prüfung und einen Rollback-Test, den Sie wiederholen können, wenn der Artikelablauf wächst.
Quellen
- Transaktionen — node-postgres.
- pg.Pool — node-postgres.
- Abfragen — node-postgres.
- Verbindung herstellen — node-postgres.
- Pooling — node-postgres.
- BEGIN — PostgreSQL Global Development Group.
- CREATE TABLE — PostgreSQL Global Development Group.
- 13.2. Transaktionsisolation — PostgreSQL Global Development Group.
- SET TRANSACTION — PostgreSQL Global Development Group.
- 13.5. Behandlung von Serialisierungsfehlern — PostgreSQL Global Development Group.