Houd één node-postgres-transactie bij elkaar door één client op te halen met pool.connect(), elke instructie via die client uit te voeren en deze na commit of rollback vrij te geven. Geef die client ook door aan helpers. De officiële transactiegids vereist deze aanpak.
Deze tutorial is bedoeld voor een ontwikkelaar die Terminal op een Mac gebruikt om meerdere wijzigingen aan artikelrijen gezamenlijk te laten slagen of mislukken. De tutorial bevat een volledig programma met synthetische artikelrijen, een bewuste rollback-test en een diagnostische reeks voor queries die in helperfuncties verborgen zitten.
Waarom verbreekt pool.query() de transactiescope?
Een pool beheert verbindingen; hij zorgt er niet voor dat elke query onderdeel is van één transactie. Elke aanroep van pool.query() haalt intern een beschikbare client op. Opeenvolgende aanroepen gebruiken daarom niet gegarandeerd dezelfde verbinding. De node-postgres Pool API waarschuwt uitdrukkelijk om deze methode niet voor transacties te gebruiken.
Als je bijvoorbeeld BEGIN via de pool verstuurt en daarna via de pool een insert uitvoert, valt die insert niet gegarandeerd binnen de transactie die je hebt gestart. PostgreSQL voert instructies buiten een expliciet transactieblok normaal gesproken elk in hun eigen transactie uit. Een latere rollback op een andere verbinding kan die onafhankelijk vastgelegde wijzigingen niet ongedaan maken. Zie de documentatie over BEGIN van PostgreSQL.
De relevante grens is het uitgecheckte clientobject. De naam van een helper, de module waarin die staat en het feit dat de aanroeper erop wacht, bepalen niet of die helper deelneemt aan de transactie. Een helper neemt deel wanneer zijn queries de client gebruiken waarmee de transactie is gestart. Dit volgt rechtstreeks uit de vereiste om dezelfde client te gebruiken in de transactiegids.
Voer een volledig voorbeeld met commit en rollback uit op je Mac
Je hebt Node.js met npm, een actieve PostgreSQL-server en een bestaande lokale ontwikkelingsdatabase nodig. In deze walkthrough gaan we ervan uit dat die database article_demo heet en dat je normale lokale verbindingsinstellingen al werken. Het installeren van de database valt buiten deze transactietaak.
Maak in Terminal een nieuwe werkmap en installeer de driver:
mkdir pg-transaction-demo
cd pg-transaction-demo
npm init -y
npm install pg
Sla het volgende volledige programma op als transaction-demo.mjs. De tijdelijke tabel bevat uitsluitend synthetische artikel-ID's en titels. PostgreSQL-temporary tables blijven normaal gesproken bestaan na commits en worden verwijderd wanneer de sessie eindigt; dankzij die eigenschappen kan het programma een geslaagde batch controleren nadat een tweede batch is teruggedraaid. Zie CREATE TABLE.
De onderstaande transactiehelper volgt de gedocumenteerde volgorde BEGIN, COMMIT en ROLLBACK. De helper handelt ook expliciet af wanneer de rollback zelf mislukt, en bewaart beide fouten in plaats van de oorspronkelijke fout te vervangen. Querywaarden gebruiken de tekst-en-waarden-API die wordt beschreven onder Queries.
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;
});
Voer het programma vanuit dezelfde map uit:
PGDATABASE=article_demo node transaction-demo.mjs
De pool leest de omgevingsvariabelen voor PostgreSQL-verbindingen. Als je lokale setup andere verbindingsinstellingen vereist, geef dan de juiste waarden voor PGHOST en PGPORT mee naast PGDATABASE. De officiële verbindingsgids beschrijft deze variabelen en hun standaardwaarden.
Bij een geslaagde uitvoering verwacht je Commit and rollback checks passed. gevolgd door de twee oorspronkelijke artikelrijen. De tweede batch voegt article-003 in en probeert daarna article-001 opnieuw te gebruiken. Die duplicaat veroorzaakt SQLSTATE 23505, in Afhandeling van serialisatiefouten gedocumenteerd als unique_violation. Door de batch terug te draaien, wordt ook de eerdere insert verwijderd; de controleassertie verifieert dat article-003 ontbreekt.
De succes-, werkfout- en rollbackfoutpaden van de transactiehelper zijn gecontroleerd met een deterministische mockclient. Het databasevoorbeeld is hier niet uitgevoerd, omdat de omgeving het pakket pg en een PostgreSQL-testverbinding niet bevatte; de bovenstaande uitvoer beschrijft het verwachte resultaat.
Hoe delen helpers de transactieclient?
Geef de client expliciet door in de hele aanroepketen. In het programma haalt main() de client op, geeft inTransaction() hetzelfde object door aan de callback en geeft insertBatch() het door aan insertArticle(). Geen van beide inserthhelpers haalt zelf een verbinding op.
Houd voor deze implementatie de verantwoordelijkheden duidelijk:
| Functie | Verantwoordelijkheid |
|---|---|
main() |
De client ophalen, de demo instellen, de client vrijgeven en de pool van het script sluiten. |
inTransaction() |
Eén transactie starten en afronden op de opgegeven client. |
insertBatch() |
Op elke artikelbewerking wachten en de opgegeven client doorgeven. |
insertArticle() |
De insert uitvoeren via de opgegeven client. |
Het contract voor de helper is bewust beperkt: roep inTransaction() alleen aan wanneer die client geen actieve transactie heeft en laat fouten uit de callback doorgeven. BEGIN aanroepen binnen een bestaande PostgreSQL-transactie maakt geen onafhankelijke geneste transactie aan; PostgreSQL beschrijft savepoints als het mechanisme voor genest werk. Zie BEGIN.
Wacht op elke bewerking die bij de transactie hoort voordat je terugkeert uit de callback. In dit voorbeeld maakt de sequentiële lus dat voltooiingsmoment expliciet. Als je asynchrone inserts start zonder hun promises terug te geven of erop te wachten, kan de callback eindigen voordat alle resultaten zijn verwerkt.
Wat moet er gebeuren als een query of rollback mislukt?
Een gewone queryfout komt terecht in het catch-blok van de transactiehelper, dat probeert de rollback uit te voeren op dezelfde client en de oorspronkelijke fout opnieuw opwerpt. De bewuste duplicaatfout wordt pas geaccepteerd nadat die rollback is voltooid. Als de rollback mislukt, werpt de helper een AggregateError; de controle op de duplicaatfout accepteert die niet, waardoor de uitvoering bij de buitenste foutafhandeling terechtkomt.
Bij een onverwachte fout vernietigt het voorbeeld de client uit voorzorg met client.release(true). Op het normale pad geeft het de client vrij met client.release(false). Het argument voor vernietigen wordt beschreven in de Pool API.
Geef een client die je hebt opgehaald altijd vrij. Anders kunnen latere aanroepen wachten op een verbinding die nooit beschikbaar komt. Sluit de pool wanneer dit standalone script klaar is; in een applicatie hoort het sluiten van de pool bij het afsluiten van de applicatie. De gids over pooling beschrijft de vereisten voor vrijgeven en hoe pool.end() wacht tot uitgecheckte clients zijn teruggegeven.
Garandeert één client een stabiele momentopname?
Continuïteit van de client en het isolatieniveau beantwoorden verschillende vragen. De client bepaalt welke transactie je instructies ontvangt. Het isolatieniveau bepaalt wat die instructies kunnen zien terwijl andere transacties lopen. De documentatie van PostgreSQL over transactie-isolatie beschrijft deze grenzen:
| Isolatieniveau | Relevante beperking |
|---|---|
READ COMMITTED |
De standaardwaarde. Opeenvolgende gewone leesbewerkingen kunnen verschillende gelijktijdig vastgelegde gegevens zien. Elke instructie krijgt een nieuwe momentopname. |
REPEATABLE READ |
Gebruikt een momentopname die wordt vastgelegd bij de eerste instructie die geen transactiebeheerinstructie is. PostgreSQL voorkomt fantoomlezingen, maar anomalieën in de serialisatie blijven mogelijk. |
SERIALIZABLE |
Vastgelegde serialiseerbare transacties gedragen zich consistent met een bepaalde seriële uitvoering; een transactie kan mislukken en opnieuw moeten worden geprobeerd. |
READ UNCOMMITTED |
Werkt in PostgreSQL hetzelfde als READ COMMITTED. |
Leesbewerkingen kunnen ook wijzigingen zien die eerder door hun eigen transactie zijn aangebracht. Je eigen insert zien betekent dus niet dat alle latere leesbewerkingen een bevroren beeld hebben van wijzigingen uit andere transacties. Zie Transactie-isolatie.
Het voorbeeld selecteert expliciet READ COMMITTED in de openingsinstructie. Als je een ander niveau wilt gebruiken, wijzig die instructie dan voordat je de transactie uitvoert. PostgreSQL staat transactie-eigenschappen toe in BEGIN; als je in plaats daarvan SET TRANSACTION gebruikt, moet je het isolatieniveau instellen voordat je de eerste query of gegevenswijzigende instructie uitvoert. Zie BEGIN en SET TRANSACTION.
Als je REPEATABLE READ of SERIALIZABLE kiest, houd dan rekening met SQLSTATE 40001. Probeer de volledige transactie opnieuw, inclusief de logica die instructies en waarden kiest, in plaats van alleen de mislukte query opnieuw af te spelen. Het voorbeeld geeft zulke fouten bewust door in plaats van opnieuw proberen te implementeren. Afhandeling van serialisatiefouten van PostgreSQL licht deze vereiste toe en vermeldt dat nieuwe pogingen opnieuw kunnen mislukken.
Hoe vind je een query die buiten de transactie is uitgevoerd?
Volg één bedoelde batch vanaf het ophalen van de verbinding tot de uiteindelijke commit of rollback. Gebruik deze reeks om je code te controleren:
- Zoek de aanroep van
pool.connect()en stel vast welke client deze teruggeeft. - Volg elke helper die wordt aangeroepen tussen
BEGINen het afronden van de transactie. Controleer of elke helper die client ontvangt en gebruikt. - Controleer gedeelde querywrappers op verborgen aanroepen van
pool.query()of extra verbindingen die worden opgehaald. - Controleer of op elke bewerking wordt gewacht en of helpers fouten doorgeven.
- Pas de synthetische test met de dubbele rij aan voor jouw batch en controleer of een eerdere insert verdwijnt nadat een latere instructie mislukt.
Een geslaagde rollback-test levert bruikbaar bewijs voor het geteste pad, maar bewijst niet dat elke vertakking van de applicatie de juiste client gebruikt. Controleer ook conditionele helpers. Als queries op één client blijven maar herhaalde leesbewerkingen toch verschillen, vergelijk dat gedrag dan met het geselecteerde isolatieniveau voordat je de verbindingsafhandeling wijzigt.
Houd het ophalen en vrijgeven zichtbaar, geef de client expliciet door en zorg dat de transactie pas wordt afgerond nadat alle bedoelde bewerkingen zijn voltooid. Zo beschik je over een concrete structuur die je kunt controleren en een rollback-test die je kunt herhalen wanneer de artikelworkflow groeit.
Bronnen
- Transacties — node-postgres.
- pg.Pool — node-postgres.
- Queries — node-postgres.
- Verbinding maken — node-postgres.
- Pooling — node-postgres.
- BEGIN — PostgreSQL Global Development Group.
- CREATE TABLE — PostgreSQL Global Development Group.
- 13.2. Transactie-isolatie — PostgreSQL Global Development Group.
- SET TRANSACTION — PostgreSQL Global Development Group.
- 13.5. Afhandeling van serialisatiefouten — PostgreSQL Global Development Group.