PostgreSQL non crea una transazione annidata quando invii un secondo BEGIN. Visualizza un avviso e mantiene la transazione corrente; il primo COMMIT riuscito la termina. Usa i savepoint quando una funzione ausiliaria deve eseguire un rollback parziale. PostgreSQL: BEGIN
Questa guida aiuta uno sviluppatore che lavora nel Terminale Mac a diagnosticare una funzione ausiliaria che esegue il commit troppo presto di un’operazione di modifica di un articolo. Gli esempi usano righe di articoli sintetiche e mostrano come mantenere un unico responsabile della transazione, consentendo a una funzione ausiliaria di annullare il proprio batch. Seguono la documentazione aggiornata di PostgreSQL e node-postgres.
Cosa accade quando BEGIN e COMMIT compaiono all’interno di un’altra transazione?
I comandi SQL per le transazioni operano sulla transazione corrente della connessione. PostgreSQL non conta le chiamate ripetute a BEGIN in attesa delle corrispondenti chiamate a COMMIT. Un secondo BEGIN lascia invariata la transazione; un COMMIT semplice eseguito correttamente applica il commit alla transazione corrente. L’invio successivo di un altro COMMIT semplice genera un avviso perché non è aperta alcuna transazione. PostgreSQL: BEGIN, PostgreSQL: COMMIT
Per una transazione corretta, la sequenza dei confini è la seguente:
| Comando inviato su una connessione | Confine risultante |
|---|---|
Primo BEGIN |
Apre la transazione |
| Primo inserimento dell’articolo | Appartiene a quella transazione |
Secondo BEGIN |
Genera un avviso; non crea un confine interno |
| Secondo inserimento dell’articolo | Appartiene alla stessa transazione |
Primo COMMIT |
Applica il commit a entrambi gli inserimenti e termina la transazione |
| Un’altra istruzione inviata separatamente | Viene eseguita nella propria transazione con il normale comportamento di autocommit |
Un successivo ROLLBACK |
Non può annullare gli inserimenti di cui è già stato eseguito il commit |
Questo spiega un errore comune nelle applicazioni: il chiamante apre una transazione, poi richiama una funzione ausiliaria che invia i propri BEGIN e COMMIT. Anche il COMMIT della funzione ausiliaria termina la transazione del chiamante. Un’eccezione successiva nel chiamante non può ripristinare l’operazione atomica originale. Questa conclusione deriva dai confini delle transazioni documentati da PostgreSQL. PostgreSQL: Transactions
Riprodurre il commit anticipato nel Terminale Mac
Ti servono un database PostgreSQL in esecuzione e il comando psql installato, con impostazioni di connessione funzionanti. Apri Terminal e avvia una nuova sessione con psql -X. L’opzione -X ignora i file di avvio, rendendo questa dimostrazione meno dipendente dalle personalizzazioni locali. Esegui lo script seguente nella sessione; abilita esplicitamente l’autocommit. PostgreSQL: psql
La tabella è temporanea e contiene solo titoli di articoli sintetici. ON COMMIT PRESERVE ROWS mantiene le righe disponibili per l’ispezione attraverso i confini delle transazioni; PostgreSQL elimina la tabella temporanea al termine della sessione. La dimostrazione riguarda il comportamento del rollback all’interno della sessione, non la persistenza dei dati dopo la disconnessione. 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;
Il risultato atteso contiene tutte e tre le righe. Il secondo BEGIN avvisa che una transazione è già in corso. Il COMMIT applica il commit alle righe 1 e 2; la riga 3 viene poi confermata tramite autocommit. L’ultimo ROLLBACK non trova alcuna transazione aperta da annullare. Sono risultati attesi dedotti dal comportamento documentato, non l’esito di un’esecuzione del database dichiarata. PostgreSQL: BEGIN, PostgreSQL: Transactions
Mantieni questa riproduzione in psql interattivo, come mostrato. Il modo in cui il client invia le istruzioni è importante: più istruzioni SQL passate insieme in un’unica stringa con psql -c possono condividere una transazione implicita, a meno che comandi espliciti di transazione non le separino. La documentazione consiglia comandi separati o lo standard input quando si desidera un’esecuzione separata. PostgreSQL: psql
In che modo i savepoint preservano la transazione esterna?
Un savepoint segna una posizione all’interno di una transazione esistente. ROLLBACK TO SAVEPOINT elimina le modifiche successive a quella posizione, preservando il lavoro precedente. RELEASE SAVEPOINT rimuove il contrassegno e mantiene le modifiche sopravvissute all’interno della transazione contenitore. Non applica il commit di queste modifiche in modo indipendente. PostgreSQL: SAVEPOINT, PostgreSQL: RELEASE SAVEPOINT
Esegui questo esempio completo nella stessa sessione o in un’altra sessione psql nuova. Crea una propria tabella temporanea:
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;
Le righe attese sono 1 e 3. La riga 2 viene eliminata al savepoint, mentre le righe iniziale e finale restano parte di un’unica transazione fino al suo COMMIT finale. Sostituendo questo COMMIT finale con ROLLBACK, verrebbero eliminati entrambi gli inserimenti sopravvissuti. PostgreSQL: SAVEPOINT, PostgreSQL: Transactions
La durata dei savepoint è importante anche quando si compongono funzioni ausiliarie. Il rollback a un savepoint lascia disponibile quel savepoint con nome, ma elimina i savepoint successivi. Il rilascio di un savepoint lo rimuove insieme a tutti i savepoint attivi creati dopo di esso. L’esempio rilascia esplicitamente il contrassegno dopo il ripristino. PostgreSQL: ROLLBACK TO SAVEPOINT, PostgreSQL: RELEASE SAVEPOINT
Come devono condividere la transazione le funzioni ausiliarie di node-postgres?
node-postgres richiede che tutte le istruzioni di una transazione usino la stessa istanza client e non offre un’astrazione automatica per l’annidamento delle transazioni. Passa alle funzioni ausiliarie il client del chiamante. Se l’applicazione usa un pool, acquisisci un client con pool.connect(), usa quel client per tutta l’operazione e rilascialo al termine; pool.query() può indirizzare le query successive a client diversi. node-postgres: Transactions, node-postgres: Pool
Il programma completo seguente usa un unico Client diretto. Il chiamante è responsabile di BEGIN, COMMIT e dell’intero ROLLBACK. La funzione ausiliaria gestisce un savepoint intorno a un batch facoltativo di due righe. Il suo secondo inserimento ripete deliberatamente l’ID articolo 1, causando un errore di unicità; il ripristino elimina l’intero batch facoltativo, compreso il primo inserimento riuscito.
Configurare ed eseguire l’esempio completo
Usa una versione attualmente supportata di Node.js con npm installato. In una cartella di lavoro, esegui npm install pg, poi salva il programma seguente come article-transactions.mjs. Usa l’ambiente di connessione PostgreSQL esistente, comprese le variabili PGHOST, PGPORT, PGDATABASE e PGUSER, se configurate. Prima di eseguire l’esempio, queste impostazioni devono identificare un database funzionante. 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
})
Esegui node article-transactions.mjs. L’output atteso indica Optional batch kept: false e mostra gli ID articolo 1 e 3. I valori usano l’interfaccia documentata dei parametri query(text, values). Il codice JavaScript è stato verificato sintatticamente con Node.js; lo scenario del database è stato confrontato con la documentazione, ma non è stato eseguito qui. node-postgres: Queries
Ripristinare soltanto gli errori esplicitamente accettati
PostgreSQL assegna 23505 a unique_violation e consiglia di controllare i codici di errore invece del testo del messaggio. In questo caso, accettare quel codice è una regola illustrativa per il batch sintetico facoltativo. Gli altri errori vengono propagati al chiamante, che tenta di annullare l’intera transazione. La funzione ausiliaria restituisce un risultato esplicito, così il chiamante può sapere che il lavoro facoltativo è stato eliminato. PostgreSQL: PostgreSQL Error Codes
Il solo fatto di intercettare un’eccezione JavaScript non ripristina una transazione PostgreSQL non riuscita. Per recuperarla occorre eseguire il rollback a un savepoint precedentemente stabilito oppure un rollback completo. In particolare, una transazione interrotta non può essere recuperata semplicemente rilasciando il suo savepoint. PostgreSQL: Transactions, PostgreSQL: RELEASE SAVEPOINT
La funzione ausiliaria presuppone una transazione attiva e l’uso sequenziale di questo client. Non è un wrapper generale per transazioni annidate ricorsivamente: un’astrazione riutilizzabile per l’annidamento richiederebbe anche una gestione esplicita della titolarità e dei nomi dei savepoint.
Quali limiti di isolamento restano dopo aver corretto i confini?
I savepoint consentono un rollback parziale all’interno della transazione corrente; non definiscono una politica di isolamento separata. Scegli il livello di isolamento al confine esterno. PostgreSQL non consente di modificarlo dopo la prima query o istruzione di modifica dei dati della transazione, quindi una funzione ausiliaria non può elevare il livello di una transazione già utilizzata inviando un altro BEGIN. PostgreSQL: SAVEPOINT, PostgreSQL: SET TRANSACTION
Per le query ordinarie, tieni presenti queste distinzioni:
| Livello di isolamento | Limite rilevante |
|---|---|
READ COMMITTED |
Le query successive possono vedere nuove modifiche confermate da altre transazioni. Una singola transazione non implica uno snapshot fisso. |
REPEATABLE READ |
Le query condividono lo snapshot stabilito dalla prima istruzione che non riguarda il controllo della transazione, pur vedendo le proprie modifiche precedenti. Sono ancora possibili errori di serializzazione. |
SERIALIZABLE |
Le transazioni confermate correttamente producono un effetto coerente con un qualche ordine seriale. Le applicazioni devono comunque gestire gli errori di serializzazione. |
READ UNCOMMITTED |
PostgreSQL lo tratta come READ COMMITTED. |
PostgreSQL: Transaction Isolation, PostgreSQL: SET TRANSACTION
Un errore di serializzazione richiede di ripetere l’intera transazione, comprese le decisioni che hanno prodotto le istruzioni SQL. Ripetere soltanto il lavoro del savepoint della funzione ausiliaria non aggiorna lo snapshot della transazione esterna. PostgreSQL identifica gli errori di serializzazione con SQLSTATE 40001. Il programma dimostrativo propaga questi errori; non implementa tentativi automatici. PostgreSQL: Transaction Isolation
Come scegliere il confine corretto per una funzione ausiliaria?
Quando esamini un chiamante e le relative funzioni ausiliarie per gli articoli, segui questa sequenza decisionale:
- Tutto deve riuscire insieme: lascia che il chiamante gestisca la transazione e passa il suo client alle funzioni ausiliarie, che eseguono solo query.
- È possibile scartare un batch mentre il chiamante prosegue: stabilisci un savepoint prima del batch, ripristina lo stato soltanto per gli errori accettati e restituisci esplicitamente l’esito.
- Il batch deve essere confermato in modo indipendente: usa un client e una transazione separati. Il relativo commit sopravvive quindi al rollback esterno, perciò questa soluzione non può soddisfare l’operazione atomica originale.
- Un’esecuzione concorrente produce un errore di serializzazione: riavvia l’intera transazione invece di trattare la funzione ausiliaria come un confine di tentativi indipendente.
Queste scelte seguono la semantica dei savepoint di PostgreSQL e il requisito di node-postgres secondo cui una transazione deve restare sullo stesso client. PostgreSQL: SAVEPOINT, node-postgres: Transactions, PostgreSQL: Transaction Isolation
Per l’operazione di modifica di articoli illustrata qui, mantieni un unico responsabile della transazione, passa un solo client lungo la catena di chiamate e assegna un savepoint al lavoro facoltativo. Esamina i comandi di transazione di ogni funzione ausiliaria: un COMMIT inatteso modifica il confine per l’intera connessione, mentre il rilascio di un savepoint lascia comunque al chiamante la responsabilità dell’esito finale.
Fonti
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