Markera först vilka databasändringar som måste lyckas tillsammans. Starta sedan transaktionen före den första skrivningen och avsluta den efter den sista. Vid minsta fel ska undantaget lämna blocket och hela operationen rullas tillbaka. Beviset är inte ännu ett lyckat test, utan ett felprov som medvetet stoppar flödet mellan två steg och därefter kontrollerar tre saker: ingen order finns, lagersaldot är oförändrat och ingen händelse har sparats.
Tre gröna skrivningar kan ge ett trasigt resultat
Antag att en order på 2 exemplar ska skapas när lagret står på 10. Den AI-genererade servicen gör följande:
def skapa_order(db, artikel_id, antal):
order_id = db.insert_order(artikel_id, antal)
db.commit()
db.reserve_stock(artikel_id, antal)
db.commit()
db.insert_event(order_id, "order_created")
db.commit()
return order_id
Om alla tre anrop lyckas ser resultatet rätt ut: en order finns, lagret är 8 och händelsen finns. Testet som anropar funktionen en gång och jämför svaret med ett id kan därför vara grönt utan att säga något om atomariteten. Tvinga däremot den tredje skrivningen att misslyckas. Då finns ordern kvar, lagret är fortfarande 8 och händelsen saknas. Systemet har hamnat i ett tillstånd som affärsregeln aldrig tillät.
Den farliga detaljen behöver inte vara tre synliga commit(). PostgreSQL behandlar varje SQL-sats som en transaktion; utan ett uttryckligt BEGIN får varje lyckad fristående sats i praktiken en egen COMMIT [1]. PostgreSQL kallar själv det beteendet autocommit [2], och många drivrutiner och ramverk döljer dessutom transaktionskommandona bakom autocommit eller sessionshantering. Läs därför inte bara diffen. Kontrollera även anslutningens standardläge och om någon hjälpfunktion committar åt anroparen.
Här räcker ett enda request och ett enda avsiktligt undantag för att återskapa felet. Två anrop behöver inte tävla om samma rad. Förlorade uppdateringar, unika index och radlås hör till samtidighetsbuggar i AI-genererad kod. En transaktion kan vara en del av lösningen även där, men felprovet och frågan du ställer är andra.
Rita gränsen efter affärsregeln, inte efter filerna
PostgreSQL beskriver en transaktion som flera steg samlade till en enda allt-eller-inget-operation: om ett fel hindrar den från att slutföras ska inget av stegen påverka databasen [1]. För orderfallet ger det en enkel fråga för varje skrivning:
- Får en order finnas utan reservationen? Nej.
- Får lagret minska utan att ordern finns? Nej.
- Får operationen räknas som genomförd utan den obligatoriska händelsen? Nej.
De tre svaren är samma, alltså hör skrivningarna till samma transaktionsgräns. Gränsen ägs av servicefunktionen som känner till hela operationen. En låg nivå som insert_order() vet bara hur en rad skapas och ska normalt inte fatta beslut om när hela affärsoperationen är klar.
def skapa_order(db, event_store, artikel_id, antal):
with db.transaction() as tx:
order_id = tx.insert_order(artikel_id, antal)
reserverade = tx.reserve_stock(artikel_id, antal)
if reserverade != 1:
raise OutOfStock(artikel_id)
event_store.add(tx, order_id, "order_created")
return order_id
db.transaction() är illustrativ syntax; ditt bibliotek kan heta exempelvis atomic, begin eller använda en sessionskontext. Egenskaperna är viktigare än namnet: samma databasanslutning används i alla tre anrop, commit sker först när blocket lämnas utan fel och ett undantag ut ur blocket utlöser rollback.
Kontrollen av reserverade är också en del av gränsen. En UPDATE som träffar 0 rader behöver inte kasta ett tekniskt fel. Om 0 betyder att lagret inte räckte måste applikationen själv göra det till ett fel innan transaktionen committas. Annars får du samma delresultat trots att varje SQL-sats var tekniskt lyckad.
Sök efter commit, autocommit-inställningar och hjälpfunktioner som öppnar en egen session. En snygg with transaction runt servicefunktionen hjälper inte om insert_order() använder en annan anslutning eller gör en oberoende commit. Ett sådant anrop ligger utanför den gräns som koden ser ut att ha.
Skriv provet som går sönder mellan steg två och tre
Det relevanta testet ska köras mot en riktig testdatabas. Om alla databaslager är mockade kan testet bara kontrollera att tre metoder anropades i rätt ordning; det kan inte se vad som faktiskt blev committat. Gör i stället den sista komponenten till en kontrollerad felpunkt och läs sedan tillbaka varje bestående effekt:
class FailingEventStore:
def add(self, tx, order_id, event_type):
raise ForcedFailure("fel efter lagerreservationen")
def test_inget_delresultat_ligger_kvar(db):
article_id = seed_stock(db, quantity=10)
with pytest.raises(ForcedFailure):
skapa_order(
db,
FailingEventStore(),
article_id,
antal=2,
)
assert db.count_orders(article_id) == 0
assert db.stock_quantity(article_id) == 10
assert db.count_events() == 0
Testet placerar felet efter att två skrivningar har hunnit köras men före commit. Det är viktigt. Ett fel före första skrivningen bevisar ingenting, och ett fel efter att transaktionsblocket har lämnat kan inte längre påverka en redan genomförd commit. PostgreSQL anger att ROLLBACK i stället för COMMIT tar bort uppdateringarna som gjorts hittills i blocket [1].
Läs också tillbaka datan genom en ny fråga efter att undantaget fångats. Att återanvända ett redan laddat ORM-objekt kan visa ett gammalt värde ur minnet. Testets påstående gäller bestående databastillstånd, så det ska fråga databasen igen. Behöver testmiljön själv omsluta varje test i en yttre transaktion ska du dessutom säkerställa att servicekoden verkligen får använda en inre rollback på det sätt produktionen gör.
Kör testet först mot den trasiga versionen. Det ska falla på minst ordern och lagersaldot. Lägg sedan på transaktionsgränsen och kör igen. Då har du ett regressionstest som skiljer den saknade gränsen från den rättade koden, inte bara ett grönt exempel som båda versionerna klarar.
Rollback kräver att felet får lämna blocket
En vanlig andra AI-bugg är att transaktionen finns men undantaget fångas för tidigt:
def skapa_order(db, event_store, artikel_id, antal):
with db.transaction() as tx:
order_id = tx.insert_order(artikel_id, antal)
try:
tx.reserve_stock(artikel_id, antal)
event_store.add(tx, order_id, "order_created")
except Exception:
logger.exception("orderflödet misslyckades")
return order_id
Om transaktionshanteraren tolkar ett normalt avslut som lyckat kan blocket committas trots loggraden. Fånga bara fel om du kan översätta dem och sedan kasta vidare, eller markera transaktionen explicit för rollback enligt ramverkets dokumentation. Det breda blocket ovan är samma grundproblem som i undantaget som sväljs i AI-genererad kod, men konsekvensen här är bestående deldata.
Ett annat misstag är att lägga en egen transaktion runt varje hjälpfunktion. Då får du tre välformade transaktioner som var och en fungerar exakt som utlovat, men ingen transaktion för operationen som användaren faktiskt bad om. Räkna därför affärsoperationer, inte antalet BEGIN i diffen.
Databasen kan inte återkalla ett externt anrop
Transaktionsgränsen gäller databasen. Om steg tre i stället skickar ett mejl, debiterar ett kort eller anropar ett lager-API kan en rollback inte göra det externa anropet ogjort. Håll därför den här artikeln till beroende skrivningar i samma databas. För timeout, omförsök och dubbla anrop finns den separata genomgången av API-integrationer med AI.
Ett vanligt sätt att koppla ihop världarna är en outbox: skriv ordern, reservationen och en rad som beskriver det framtida externa anropet i samma databastransaktion. En separat arbetare levererar sedan meddelandet med idempotens och omförsök. Databasdelen blir atomär; den externa leveransen blir spårbar och återkörbar. Det är två garantier, inte en magisk distribuerad rollback.
Ändrar diffen samtidigt tabeller eller backfill är det ytterligare ett eget förlopp. Schemaändring, kompatibilitet mellan versioner och återställning av migreringar behandlas i databasmigreringar med AI. Kandidaten här utgår från ett redan etablerat schema och en vanlig request som skriver flera beroende rader.
INSERT, UPDATE och DELETE som operationen gör, även bakom repositories och ORM-metoder.
[1] PostgreSQL 18, Transactions — allt-eller-inget-egenskapen, BEGIN, COMMIT, ROLLBACK och att varje fristående SQL-sats annars körs som en egen transaktion.
[2] PostgreSQL 18, START TRANSACTION — kommandot som startar ett transaktionsblock, dess likvärdighet med BEGIN och PostgreSQLs autocommit-beteende utanför ett sådant block.
Källorna lästa 24 augusti 2026. Python-API:erna i exemplen är avsiktligt förenklade och illustrativa; kontrollera transaktions- och nästlingbeteendet i det bibliotek som din applikation faktiskt använder.