PostgreSQL skapar inte en nästlad transaktion när du skickar ett andra BEGIN. Det ger en varning och behåller den aktuella transaktionen; den första lyckade COMMIT avslutar den. Använd savepoints när en hjälpfunktion behöver göra en partiell återställning. PostgreSQL: BEGIN
Den här guiden hjälper en utvecklare som arbetar i Mac Terminal att diagnostisera en hjälpfunktion som committar en artikelredigering för tidigt. Exemplen använder syntetiska artikelrader och visar hur en och samma transaktionsägare kan behållas samtidigt som en hjälpfunktion kan ångra sin egen batch. De följer den aktuella dokumentationen för PostgreSQL och node-postgres.
Vad händer när BEGIN och COMMIT används inuti en annan transaktion?
SQL-transaktionskommandon verkar på anslutningens aktuella transaktion. PostgreSQL räknar inte upprepade anrop till BEGIN och väntar på motsvarande anrop till COMMIT. Ett andra BEGIN lämnar transaktionen oförändrad; ett lyckat vanligt COMMIT committar den aktuella transaktionen. Om du därefter utfärdar ytterligare ett vanligt COMMIT visas en varning eftersom ingen transaktion är öppen. PostgreSQL: BEGIN, PostgreSQL: COMMIT
För en fungerande transaktion ser gränserna ut så här:
| Kommando som skickas på en och samma anslutning | Gräns som uppstår |
|---|---|
Första BEGIN |
Öppnar transaktionen |
| Första artikelinfogningen | Tillhör den transaktionen |
Andra BEGIN |
Ger en varning; skapar ingen inre gräns |
| Andra artikelinfogningen | Tillhör samma transaktion |
Första COMMIT |
Committar båda infogningarna och avslutar transaktionen |
| En separat efterföljande sats | Körs i en egen transaktion vid normalt autocommit-beteende |
Ett senare ROLLBACK |
Kan inte ångra de redan committade infogningarna |
Det här förklarar ett vanligt fel i applikationer: anroparen öppnar en transaktion och anropar sedan en hjälpfunktion som skickar ett eget BEGIN och COMMIT. Hjälpfunktionens COMMIT avslutar också anroparens transaktion. Ett senare undantag hos anroparen kan inte återställa den ursprungliga allt-eller-inget-operationen. Den slutsatsen följer av PostgreSQLs dokumenterade transaktionsgränser. PostgreSQL: Transactions
Återskapa den för tidiga committen i Mac Terminal
Du behöver en PostgreSQL-databas som körs och ett installerat psql-kommando med fungerande anslutningsinställningar. Öppna Terminal och starta en ny session med psql -X. Alternativet -X hoppar över startfiler och gör demonstrationen mindre beroende av lokala anpassningar. Kör följande skript i den sessionen; det aktiverar autocommit uttryckligen. PostgreSQL: psql
Tabellen är temporär och innehåller endast syntetiska artikeltitlar. ON COMMIT PRESERVE ROWS gör att raderna är tillgängliga för granskning över transaktionsgränser; PostgreSQL tar bort den temporära tabellen när sessionen avslutas. Demonstrationen visar återställningsbeteende i sessionen, inte beständig lagring efter frånkoppling. PostgreSQL: CREATE TABLE
\set AUTOCOMMIT on
CREATE TEMP TABLE article_boundary_demo (
id integer PRIMARY KEY,
title text NOT NULL
) ON COMMIT PRESERVE ROWS;
BEGIN;
INSERT INTO article_boundary_demo VALUES (1, 'Opening draft');
-- Deliberately incorrect attempt to open an inner transaction.
BEGIN;
INSERT INTO article_boundary_demo VALUES (2, 'Middle draft');
COMMIT;
-- The previous COMMIT has already ended the transaction.
INSERT INTO article_boundary_demo VALUES (3, 'Closing draft');
ROLLBACK;
SELECT id, title FROM article_boundary_demo ORDER BY id;
Det förväntade resultatet innehåller alla tre raderna. Det andra BEGIN ger en varning om att en transaktion redan pågår. COMMIT committar raderna 1 och 2; rad 3 committas sedan genom autocommit. Det sista ROLLBACK har ingen öppen transaktion att ångra. Detta är förväntade resultat utifrån det dokumenterade beteendet, inte en rapport om en databas kördes. PostgreSQL: BEGIN, PostgreSQL: Transactions
Kör den här demonstrationen i interaktiva psql, som visas. Hur klienten skickar kommandona spelar roll: flera SQL-satser som skickas tillsammans i en enda psql -c-sträng kan dela en implicit transaktion, om inte uttryckliga transaktionskommandon delar upp dem. Dokumentationen rekommenderar separata kommandon eller standardindata när separat körning är avsikten. PostgreSQL: psql
Hur bevarar savepoints den yttre transaktionen?
En savepoint markerar en position inuti en befintlig transaktion. ROLLBACK TO SAVEPOINT kastar bort ändringar efter den positionen samtidigt som tidigare arbete bevaras. RELEASE SAVEPOINT tar bort markören och behåller de ändringar som återstår i den omslutande transaktionen. Det committar inte ändringarna på egen hand. PostgreSQL: SAVEPOINT, PostgreSQL: RELEASE SAVEPOINT
Kör det här fullständiga exemplet i samma session eller i en annan ny psql-session. Det skapar en egen temporär tabell:
CREATE TEMP TABLE article_savepoint_demo (
id integer PRIMARY KEY,
title text NOT NULL
) ON COMMIT PRESERVE ROWS;
BEGIN;
INSERT INTO article_savepoint_demo VALUES (1, 'Opening draft');
SAVEPOINT optional_article;
INSERT INTO article_savepoint_demo VALUES (2, 'Optional draft');
ROLLBACK TO SAVEPOINT optional_article;
RELEASE SAVEPOINT optional_article;
INSERT INTO article_savepoint_demo VALUES (3, 'Closing draft');
COMMIT;
SELECT id, title FROM article_savepoint_demo ORDER BY id;
De förväntade raderna är 1 och 3. Rad 2 kastas bort vid savepointen, medan den inledande och avslutande raden förblir en del av en och samma transaktion fram till dess slutliga COMMIT. Om du ersätter det sista COMMIT med ROLLBACK kastas båda de infogningar som återstår bort. PostgreSQL: SAVEPOINT, PostgreSQL: Transactions
Savepointens livslängd spelar också roll när hjälpfunktioner kombineras. En återställning till en savepoint lämnar den namngivna savepointen tillgänglig, men förstör senare savepoints. Om en savepoint släpps tas den bort, liksom alla aktiva savepoints som skapats efter den. Exemplet släpper uttryckligen markören efter återställningen. PostgreSQL: ROLLBACK TO SAVEPOINT, PostgreSQL: RELEASE SAVEPOINT
Hur bör hjälpfunktioner i node-postgres dela transaktionen?
node-postgres kräver att varje sats i en transaktion använder samma klientinstans och har inget inbyggt abstraktionslager för nästlade transaktioner. Skicka anroparens klient till hjälpfunktionerna. Om applikationen använder en pool hämtar du en klient med pool.connect(), använder den klienten genomgående och frigör den efteråt; pool.query() kan skicka efterföljande frågor till olika klienter. node-postgres: Transactions, node-postgres: Pool
Följande fullständiga program använder en enda direkt Client. Anroparen äger BEGIN, COMMIT och den fullständiga ROLLBACK. Hjälpfunktionen äger en savepoint runt en valfri batch med två rader. Den andra infogningen upprepar avsiktligt artikel-ID:t 1, vilket orsakar ett unikhetsfel; återställningen kastar bort hela den valfria batchen, inklusive den första infogningen som lyckades.
Konfigurera och kör det fullständiga exemplet
Använd en aktuell version av Node.js som stöds och ha npm installerat. Kör npm install pg i en arbetsmapp och spara sedan programmet nedan som article-transactions.mjs. Det använder den befintliga miljön för PostgreSQL-anslutningen, inklusive PGHOST, PGPORT, PGDATABASE och PGUSER där de är konfigurerade. Inställningarna måste identifiera en fungerande databas innan du kör exemplet. node-postgres: Welcome, node-postgres: Connecting
import pg from 'pg'
const { Client } = pg
async function addOptionalArticles(client) {
await client.query('SAVEPOINT optional_articles')
try {
await client.query(
'INSERT INTO article_demo (id, title) VALUES ($1, $2)',
[2, 'Optional draft']
)
await client.query(
'INSERT INTO article_demo (id, title) VALUES ($1, $2)',
[1, 'Duplicate draft']
)
} catch (error) {
if (error.code !== '23505') throw error
await client.query('ROLLBACK TO SAVEPOINT optional_articles')
await client.query('RELEASE SAVEPOINT optional_articles')
return false
}
await client.query('RELEASE SAVEPOINT optional_articles')
return true
}
async function main() {
const client = new Client({ connectionTimeoutMillis: 5000 })
await client.connect()
try {
await client.query(`
CREATE TEMP TABLE article_demo (
id integer PRIMARY KEY,
title text NOT NULL
) ON COMMIT PRESERVE ROWS
`)
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED')
try {
await client.query(
'INSERT INTO article_demo (id, title) VALUES ($1, $2)',
[1, 'Opening draft']
)
const optionalKept = await addOptionalArticles(client)
console.log('Optional batch kept:', optionalKept)
await client.query(
'INSERT INTO article_demo (id, title) VALUES ($1, $2)',
[3, 'Closing draft']
)
await client.query('COMMIT')
} catch (error) {
try {
await client.query('ROLLBACK')
} catch (rollbackError) {
throw new AggregateError(
[error, rollbackError], 'Transaction and rollback failed'
)
}
throw error
}
const result = await client.query(
'SELECT id, title FROM article_demo ORDER BY id'
)
console.table(result.rows)
} finally {
await client.end()
}
}
main().catch(error => {
console.error(error)
process.exitCode = 1
})
Kör node article-transactions.mjs. Den förväntade utdata visar Optional batch kept: false och artikel-ID:na 1 och 3. Värdena använder det dokumenterade parametergränssnittet query(text, values). JavaScript-koden syntaxkontrollerades med Node.js; databasscenariot granskades mot dokumentationen men kördes inte här. node-postgres: Queries
Återställ endast efter ett uttryckligen accepterat fel
PostgreSQL tilldelar 23505 till unique_violation och rekommenderar att felkoder kontrolleras i stället för felmeddelandets text. Här är den här koden en illustrativ regel för den valfria syntetiska batchen. Andra fel skickas vidare till anroparen, som försöker återställa hela transaktionen. Hjälpfunktionen returnerar ett uttryckligt resultat så att bortkastat valfritt arbete blir synligt för anroparen. PostgreSQL: PostgreSQL Error Codes
Att bara fånga ett JavaScript-undantag återställer inte en transaktion som misslyckats i PostgreSQL. Återställning kräver en återställning till en tidigare upprättad savepoint eller en fullständig rollback. En avbruten transaktion kan i synnerhet inte återställas genom att bara släppa dess savepoint. PostgreSQL: Transactions, PostgreSQL: RELEASE SAVEPOINT
Hjälpfunktionen förutsätter en aktiv transaktion och sekventiell användning av klienten. Den är inte en allmän rekursiv omslutning för transaktioner: en återanvändbar abstraktion för nästling skulle också kräva uttrycklig hantering av ägarskap och namn på savepoints.
Vilka isoleringsbegränsningar kvarstår när gränserna har åtgärdats?
Savepoints möjliggör partiell återställning i den aktuella transaktionen; de etablerar ingen separat isoleringspolicy. Välj isoleringsnivå vid den yttre gränsen. PostgreSQL tillåter inte att den ändras efter transaktionens första fråga eller dataändrande sats, så en hjälpfunktion kan inte höja isoleringsnivån för en redan använd transaktion genom att skicka ytterligare ett BEGIN. PostgreSQL: SAVEPOINT, PostgreSQL: SET TRANSACTION
För vanliga frågor bör du behålla följande åtskillnader:
| Isoleringsnivå | Relevant begränsning |
|---|---|
READ COMMITTED |
Efterföljande frågor kan se nycommittade ändringar från andra transaktioner. En transaktion innebär inte en enda fast ögonblicksbild. |
REPEATABLE READ |
Frågor delar ögonblicksbilden som upprättades vid den första satsen som inte är ett transaktionskontrollkommando, samtidigt som de ser sina egna tidigare ändringar. Serialiseringsfel kan fortfarande uppstå. |
SERIALIZABLE |
Lyckat committade transaktioner får en effekt som motsvarar någon seriell ordning. Applikationer måste fortfarande hantera serialiseringsfel. |
READ UNCOMMITTED |
PostgreSQL behandlar nivån som READ COMMITTED. |
PostgreSQL: Transaction Isolation, PostgreSQL: SET TRANSACTION
Vid ett serialiseringsfel måste hela transaktionen köras om, inklusive besluten som låg till grund för SQL-frågorna. Om du bara kör om hjälpfunktionens savepoint-arbete uppdateras inte den yttre transaktionens ögonblicksbild. PostgreSQL identifierar serialiseringsfel med SQLSTATE 40001. Demonstrationsprogrammet skickar vidare dessa fel; det implementerar inte automatiska omkörningar. PostgreSQL: Transaction Isolation
Hur väljer du rätt gräns för hjälpfunktionen?
Använd följande beslutsordning när du granskar en anropare och dess artikelrelaterade hjälpfunktioner:
- Allt måste lyckas tillsammans: låt anroparen äga transaktionen och skicka klienten till hjälpfunktioner som bara kör frågor.
- En batch kan kastas bort medan anroparen fortsätter: upprätta en savepoint före batchen, återställ bara efter accepterade fel och returnera utfallet uttryckligen.
- Batchen måste committas oberoende: använd en separat klient och transaktion. Committen överlever då en yttre rollback, vilket innebär att den inte kan uppfylla det ursprungliga allt-eller-inget-kravet.
- En samtidig körning orsakar ett serialiseringsfel: starta om hela transaktionen i stället för att behandla hjälpfunktionen som en oberoende gräns för omkörning.
Dessa val följer av PostgreSQLs semantik för savepoints och node-postgres krav på att en transaktion hålls på en klient. PostgreSQL: SAVEPOINT, node-postgres: Transactions, PostgreSQL: Transaction Isolation
För artikelredigeringsuppgiften som visas här bör du behålla en enda transaktionsägare, skicka en enda klient genom anropskedjan och ge valfritt arbete en savepoint. Granska alla hjälpfunktioners transaktionskommandon: ett oväntat COMMIT ändrar gränsen för hela anslutningen, medan en släppt savepoint fortfarande innebär att anroparen ansvarar för det slutliga resultatet.
Källor
BEGIN — PostgreSQL Global Development Group
COMMIT — PostgreSQL Global Development Group
3.4. Transactions — PostgreSQL Global Development Group
psql — PostgreSQL Global Development Group
CREATE TABLE — PostgreSQL Global Development Group
SAVEPOINT — PostgreSQL Global Development Group
RELEASE SAVEPOINT — PostgreSQL Global Development Group
ROLLBACK TO SAVEPOINT — PostgreSQL Global Development Group
SET TRANSACTION — PostgreSQL Global Development Group
13.2. Transaction Isolation — PostgreSQL Global Development Group
Appendix A. PostgreSQL Error Codes — PostgreSQL Global Development Group
Transactions — node-postgres
Pool — node-postgres
Welcome — node-postgres
Connecting — node-postgres
Queries — node-postgres