PostgreSQL nie tworzy zagnieżdżonej transakcji, gdy wyślesz drugie BEGIN. Ostrzega i pozostawia bieżącą transakcję bez zmian; pierwsze udane COMMIT ją kończy. Użyj punktów zapisu, gdy funkcja pomocnicza musi częściowo wycofać zmiany. PostgreSQL: BEGIN
Ten przewodnik pomaga programiście pracującemu w Terminalu na Macu zdiagnozować funkcję pomocniczą, która zbyt wcześnie zatwierdza operację edycji artykułu. Przykłady wykorzystują syntetyczne wiersze artykułów i pokazują, jak zachować jednego właściciela transakcji, a jednocześnie umożliwić funkcji pomocniczej wycofanie własnej partii zmian. Są zgodne z aktualną dokumentacją PostgreSQL i node-postgres.
Co się dzieje, gdy BEGIN i COMMIT pojawiają się wewnątrz innej transakcji?
Polecenia transakcyjne SQL działają na bieżącej transakcji połączenia. PostgreSQL nie zlicza kolejnych wywołań BEGIN i nie czeka na pasujące wywołania COMMIT. Drugie BEGIN pozostawia transakcję bez zmian; udane zwykłe COMMIT zatwierdza tę bieżącą transakcję. Wydanie później kolejnego zwykłego COMMIT powoduje ostrzeżenie, ponieważ żadna transakcja nie jest otwarta. PostgreSQL: BEGIN, PostgreSQL: COMMIT
W prawidłowej transakcji przebieg granic wygląda następująco:
| Polecenie wysłane w jednym połączeniu | Wynikowa granica |
|---|---|
Pierwsze BEGIN |
Otwiera transakcję |
| Pierwsze wstawienie artykułu | Należy do tej transakcji |
Drugie BEGIN |
Ostrzega; nie tworzy wewnętrznej granicy |
| Drugie wstawienie artykułu | Należy do tej samej transakcji |
Pierwsze COMMIT |
Zatwierdza oba wstawienia i kończy transakcję |
| Kolejne osobno wysłane polecenie | Działa we własnej transakcji przy standardowym zachowaniu autocommit |
Późniejsze ROLLBACK |
Nie może cofnąć już zatwierdzonych wstawień |
Wyjaśnia to częsty błąd aplikacji: wywołujący otwiera transakcję, a następnie wywołuje funkcję pomocniczą, która wysyła własne BEGIN i COMMIT. COMMIT funkcji pomocniczej kończy również transakcję wywołującego. Późniejszy wyjątek u wywołującego nie może przywrócić pierwotnej operacji typu „wszystko albo nic”. Wniosek ten wynika z udokumentowanych granic transakcji PostgreSQL. PostgreSQL: Transactions
Jak odtworzyć przedwczesne zatwierdzenie w Terminalu na Macu
Potrzebujesz działającej bazy danych PostgreSQL oraz zainstalowanego polecenia psql z poprawnymi ustawieniami połączenia. Otwórz Terminal i rozpocznij nową sesję poleceniem psql -X. Opcja -X pomija pliki startowe, dzięki czemu demonstracja jest mniej zależna od lokalnych dostosowań. Uruchom poniższy skrypt w tej sesji; jawnie włącza on autocommit. PostgreSQL: psql
Tabela jest tymczasowa i zawiera wyłącznie syntetyczne tytuły artykułów. ON COMMIT PRESERVE ROWS zachowuje jej wiersze do wglądu między granicami transakcji; PostgreSQL usuwa tabelę tymczasową po zakończeniu sesji. Demonstracja pokazuje zachowanie wycofywania zmian w ramach sesji, a nie trwałe przechowywanie danych po rozłączeniu. 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;
Oczekiwany wynik zawiera wszystkie trzy wiersze. Drugie BEGIN ostrzega, że transakcja jest już w toku. COMMIT zatwierdza wiersze 1 i 2; wiersz 3 zostaje następnie zatwierdzony przez autocommit. Końcowe ROLLBACK nie ma otwartej transakcji do cofnięcia. Są to oczekiwane wyniki wywnioskowane z udokumentowanego zachowania, a nie rezultat zgłoszonego wykonania na bazie danych. PostgreSQL: BEGIN, PostgreSQL: Transactions
Pozostaw to odtworzenie w interaktywnej sesji psql, tak jak pokazano. Sposób przesyłania poleceń przez klienta ma znaczenie: kilka poleceń SQL przekazanych razem w jednym ciągu psql -c może współdzielić niejawną transakcję, chyba że jawne polecenia transakcyjne ją podzielą. Dokumentacja zaleca osobne polecenia lub standardowe wejście, gdy zamierzone jest oddzielne wykonanie. PostgreSQL: psql
Jak punkty zapisu zachowują transakcję zewnętrzną?
Punkt zapisu oznacza pozycję wewnątrz istniejącej transakcji. ROLLBACK TO SAVEPOINT odrzuca zmiany wprowadzone po tej pozycji, zachowując wcześniejsze prace. RELEASE SAVEPOINT usuwa znacznik i zachowuje pozostałe zmiany w ramach transakcji zawierającej ten punkt. Nie zatwierdza tych zmian samodzielnie. PostgreSQL: SAVEPOINT, PostgreSQL: RELEASE SAVEPOINT
Uruchom ten kompletny przykład w tej samej sesji albo w innej, nowej sesji psql. Tworzy on własną tabelę tymczasową:
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;
Oczekiwane wiersze to 1 i 3. Wiersz 2 zostaje odrzucony przy punkcie zapisu, a wiersze otwierający i zamykający pozostają częścią jednej transakcji aż do jej końcowego COMMIT. Zastąpienie tego końcowego COMMIT przez ROLLBACK odrzuciłoby oba zachowane wstawienia. PostgreSQL: SAVEPOINT, PostgreSQL: Transactions
Cykl życia punktu zapisu ma również znaczenie przy łączeniu funkcji pomocniczych. Wycofanie do punktu zapisu pozostawia ten nazwany punkt dostępny, ale niszczy późniejsze punkty zapisu. Zwolnienie punktu zapisu usuwa go oraz wszystkie aktywne punkty utworzone po nim. W przykładzie znacznik jest jawnie zwalniany po odzyskaniu kontroli. PostgreSQL: ROLLBACK TO SAVEPOINT, PostgreSQL: RELEASE SAVEPOINT
Jak funkcje pomocnicze node-postgres powinny współdzielić transakcję?
node-postgres wymaga, aby wszystkie polecenia w transakcji korzystały z tej samej instancji klienta, i nie udostępnia automatycznej abstrakcji zagnieżdżania transakcji. Przekazuj klienta wywołującego do funkcji pomocniczych. Jeśli aplikacja korzysta z puli, pobierz klienta za pomocą pool.connect(), używaj go przez cały czas i zwolnij go później; pool.query() może kierować kolejne zapytania do różnych klientów. node-postgres: Transactions, node-postgres: Pool
Poniższy kompletny program używa jednego bezpośredniego Client. Wywołujący jest właścicielem BEGIN, COMMIT oraz pełnego ROLLBACK. Funkcja pomocnicza obejmuje opcjonalną, dwuwierszową partię punktami zapisu. Jej drugie wstawienie celowo powtarza identyfikator artykułu 1, powodując błąd unikatowości; odzyskiwanie odrzuca całą opcjonalną partię, łącznie z jej wcześniejszym udanym wstawieniem.
Przygotowanie i uruchomienie kompletnego przykładu
Użyj aktualnej, obsługiwanej wersji Node.js z zainstalowanym npm. W katalogu roboczym uruchom npm install pg, a następnie zapisz poniższy program jako article-transactions.mjs. Korzysta on z istniejącego środowiska połączenia PostgreSQL, w tym z PGHOST, PGPORT, PGDATABASE i PGUSER, jeśli są skonfigurowane. Te ustawienia muszą wskazywać działającą bazę danych przed uruchomieniem przykładu. 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
})
Uruchom node article-transactions.mjs. Oczekiwany wynik informuje Optional batch kept: false i wyświetla identyfikatory artykułów 1 i 3. Wartości korzystają z udokumentowanego interfejsu parametrów query(text, values). Kod JavaScript został sprawdzony pod kątem składni za pomocą Node.js; scenariusz bazy danych zweryfikowano na podstawie dokumentacji, ale nie został tu wykonany. node-postgres: Queries
Odzyskuj kontrolę tylko po jawnie zaakceptowanym błędzie
PostgreSQL przypisuje 23505 do unique_violation i zaleca sprawdzanie kodów błędów zamiast tekstu komunikatu. W tym przykładzie akceptacja tego kodu jest poglądową regułą dla opcjonalnej, syntetycznej partii. Inne błędy są przekazywane dalej do wywołującego, który podejmuje próbę wycofania całej transakcji. Funkcja pomocnicza zwraca jawny wynik, dzięki czemu wywołujący widzi, że opcjonalna praca została odrzucona. PostgreSQL: PostgreSQL Error Codes
Samo przechwycenie wyjątku JavaScript nie przywraca sprawności nieudanej transakcji PostgreSQL. Odzyskanie wymaga wycofania do wcześniej ustanowionego punktu zapisu albo pełnego wycofania. W szczególności transakcja w stanie przerwania nie może odzyskać sprawności przez samo zwolnienie punktu zapisu. PostgreSQL: Transactions, PostgreSQL: RELEASE SAVEPOINT
Funkcja pomocnicza zakłada aktywną transakcję i sekwencyjne użycie tego klienta. Nie jest uniwersalnym rekurencyjnym opakowaniem transakcji: wielokrotnego użytku abstrakcja zagnieżdżania wymagałaby również jawnego zarządzania własnością i nazwami punktów zapisu.
Jakie ograniczenia izolacji pozostają po naprawieniu granic?
Punkty zapisu umożliwiają częściowe wycofanie zmian w ramach bieżącej transakcji; nie ustanawiają odrębnej polityki izolacji. Wybierz poziom izolacji na zewnętrznej granicy. PostgreSQL nie zezwala na jego zmianę po pierwszym zapytaniu lub poleceniu modyfikującym dane w transakcji, więc funkcja pomocnicza nie może podnieść poziomu izolacji już używanej transakcji przez wysłanie kolejnego BEGIN. PostgreSQL: SAVEPOINT, PostgreSQL: SET TRANSACTION
W przypadku zwykłych zapytań zachowaj następujące rozróżnienia:
| Poziom izolacji | Istotne ograniczenie |
|---|---|
READ COMMITTED |
Kolejne zapytania mogą widzieć nowo zatwierdzone zmiany z innych transakcji. Jedna transakcja nie oznacza jednego stałego migawki danych. |
REPEATABLE READ |
Zapytania współdzielą migawkę ustanowioną przez pierwsze polecenie inne niż sterujące transakcją, a jednocześnie widzą własne wcześniejsze zmiany. Nadal mogą wystąpić błędy serializacji. |
SERIALIZABLE |
Pomyślnie zatwierdzone transakcje wywierają skutek zgodny z pewnym porządkiem szeregowym. Aplikacje nadal muszą obsługiwać błędy serializacji. |
READ UNCOMMITTED |
PostgreSQL traktuje ten poziom jak READ COMMITTED. |
PostgreSQL: Transaction Isolation, PostgreSQL: SET TRANSACTION
Błąd serializacji wymaga ponowienia całej transakcji, łącznie z decyzjami, które doprowadziły do wygenerowania zapytań SQL. Ponowienie tylko pracy funkcji pomocniczej objętej punktem zapisu nie odświeża migawki transakcji zewnętrznej. PostgreSQL identyfikuje błędy serializacji kodem SQLSTATE 40001. Program demonstracyjny przekazuje te błędy dalej; nie implementuje automatycznych ponowień. PostgreSQL: Transaction Isolation
Jak wybrać właściwą granicę funkcji pomocniczej?
Podczas przeglądu wywołującego i jego funkcji pomocniczych obsługujących artykuły zastosuj następującą sekwencję decyzji:
- Wszystko musi się udać razem: niech wywołujący będzie właścicielem transakcji i przekazuje swój obiekt klienta funkcjom pomocniczym, które tylko wykonują zapytania.
- Jedną partię można odrzucić, a wywołujący może kontynuować: ustanów punkt zapisu przed tą partią, odzyskuj kontrolę tylko po zaakceptowanych błędach i jawnie zwracaj wynik.
- Partia musi zostać zatwierdzona niezależnie: użyj osobnego klienta i transakcji. Jej zatwierdzenie przetrwa wtedy wycofanie zewnętrznej transakcji, więc nie spełni pierwotnego zadania typu „wszystko albo nic”.
- Równoległe wykonanie powoduje błąd serializacji: uruchom ponownie całą transakcję, zamiast traktować funkcję pomocniczą jako niezależną granicę ponowienia.
Te decyzje wynikają z semantyki punktów zapisu PostgreSQL i wymogu node-postgres, by transakcja pozostawała na jednym kliencie. PostgreSQL: SAVEPOINT, node-postgres: Transactions, PostgreSQL: Transaction Isolation
W przedstawionym tu zadaniu edycji artykułu zachowaj jednego właściciela transakcji, przekazuj jednego klienta przez cały łańcuch wywołań i obejmuj opcjonalną pracę punktem zapisu. Sprawdź polecenia transakcyjne w każdej funkcji pomocniczej: nieoczekiwane COMMIT zmienia granicę dla całego połączenia, a zwolniony punkt zapisu nadal pozostawia wywołującego odpowiedzialnym za końcowy wynik.
Źródła
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