Om du någonsin ägnat mycket tid åt Django-databastransaktionshantering, vet du hur förvirrande det kan bli. Tidigare gav dokumentationen en hel del djup, men förståelsen kom bara genom att bygga och experimentera.
Det fanns en uppsjö av dekoratörer att arbeta med, som commit_on_success
, commit_manually
, commit_unless_managed
, rollback_unless_managed
, enter_transaction_management
, leave_transaction_management
, bara för att nämna några. Som tur är, med Django 1.6 går allt utanför dörren. Du behöver verkligen bara veta om ett par funktioner nu. Och vi kommer till dem på bara en sekund. Först tar vi upp dessa ämnen:
- Vad är transaktionshantering?
- Vad är det för fel med transaktionshantering före Django 1.6?
Innan du hoppar in i:
- Vad är rätt med transaktionshantering i Django 1.6?
Och sedan ta itu med ett detaljerat exempel:
- Randexempel
- Transaktioner
- Det rekommenderade sättet
- Använda en dekoratör
- Transaktion per HTTP-förfrågan
- SavePoints
- Inkapslade transaktioner
Vad är en transaktion?
Enligt SQL-92 är "En SQL-transaktion (ibland bara kallad "transaktion") en sekvens av exekveringar av SQL-satser som är atomär med avseende på återhämtning. Med andra ord, alla SQL-satser exekveras och committeras tillsammans. På samma sätt, när de rullas tillbaka, rullas alla påståenden ihop igen.
Till exempel:
# START
note = Note(title="my first note", text="Yay!")
note = Note(title="my second note", text="Whee!")
address1.save()
address2.save()
# COMMIT
Så en transaktion är en enda arbetsenhet i en databas. Och den enskilda arbetsenheten avgränsas av en starttransaktion och sedan en commit eller en explicit rollback.
Vad är det för fel med transaktionshantering före Django 1.6?
För att kunna svara fullständigt på denna fråga måste vi ta upp hur transaktioner hanteras i databasen, klientbiblioteken och inom Django.
Databaser
Varje sats i en databas måste köras i en transaktion, även om transaktionen bara innehåller en sats.
De flesta databaser har en AUTOCOMMIT
inställning, som vanligtvis är satt till True som standard. Denna AUTOCOMMIT
lindar varje uttalande i en transaktion som omedelbart genomförs om uttalandet lyckas. Naturligtvis kan du manuellt anropa något som START_TRANSACTION
vilket tillfälligt kommer att stänga av AUTOCOMMIT
tills du ringer COMMIT_TRANSACTION
eller ROLLBACK
.
Men att ta bort här är att AUTOCOMMIT
inställningen tillämpar en implicit commit efter varje påstående .
Kundbibliotek
Sedan finns det Python-klientbiblioteken som sqlite3 och mysqldb, som gör att Python-program kan samverka med själva databaserna. Sådana bibliotek följer en uppsättning standarder för hur man kommer åt och frågar databaserna. Den standarden, DB API 2.0, beskrivs i PEP 249. Även om det kan ge en aningen torr läsning, är en viktig åtgärd att PEP 249 anger att databasen AUTOCOMMIT
ska vara AV som standard.
Detta står helt klart i konflikt med vad som händer i databasen:
- SQL-satser måste alltid köras i en transaktion, som databasen vanligtvis öppnar för dig via
AUTOCOMMIT
. - Men enligt PEP 249 bör detta inte hända.
- Klientbibliotek måste spegla vad som händer i databasen, men eftersom de inte får aktivera
AUTOCOMMIT
på som standard lindar de helt enkelt dina SQL-satser i en transaktion, precis som databasen.
Okej. Stanna hos mig lite längre.
Django
Gå in i Django. Django har också något att säga om transaktionshantering. I Django 1.5 och tidigare körde Django i princip med en öppen transaktion och auto-committerade den transaktionen när du skrev data till databasen. Så varje gång du anropade något som model.save()
eller model.update()
, Django genererade lämpliga SQL-satser och genomförde transaktionen.
Även i Django 1.5 och tidigare rekommenderades att du använde TransactionMiddleware
för att binda transaktioner till HTTP-förfrågningar. Varje begäran fick en transaktion. Om svaret returnerades utan undantag, skulle Django begå transaktionen men om din visningsfunktion gav ett fel, ROLLBACK
skulle kallas. Detta inaktiverade AUTOCOMMIT
. Om du ville ha standardtransaktionshantering för autocommit-stil på databasnivå var du tvungen att hantera transaktionerna själv - vanligtvis genom att använda en transaktionsdekorator på din visningsfunktion såsom @transaction.commit_manually
, eller @transaction.commit_on_success
.
Ta ett andetag. Eller två.
Vad betyder detta?
Ja, det händer mycket där, och det visar sig att de flesta utvecklare bara vill ha standarddatabasnivå autocommits - vilket innebär att transaktioner stannar bakom kulisserna och gör sitt tills du behöver justera dem manuellt.
Vad är rätt med transaktionshantering i Django 1.6?
Välkommen till Django 1.6. Gör ditt bästa för att glömma allt vi just pratade om och kom bara ihåg att i Django 1.6 använder du databasen AUTOCOMMIT
och hantera transaktioner manuellt vid behov. I grund och botten har vi en mycket enklare modell som i princip gör vad databasen var designad för att göra från första början.
Nog med teori. Låt oss koda.
Randexempel
Här har vi den här exempelvisningsfunktionen som hanterar registrering av en användare och anrop till Stripe för kreditkortshantering.
def register(request):
user = None
if request.method == 'POST':
form = UserForm(request.POST)
if form.is_valid():
customer = Customer.create("subscription",
email = form.cleaned_data['email'],
description = form.cleaned_data['name'],
card = form.cleaned_data['stripe_token'],
plan="gold",
)
cd = form.cleaned_data
try:
user = User.create(cd['name'], cd['email'], cd['password'],
cd['last_4_digits'])
if customer:
user.stripe_id = customer.id
user.save()
else:
UnpaidUsers(email=cd['email']).save()
except IntegrityError:
form.addError(cd['email'] + ' is already a member')
else:
request.session['user'] = user.pk
return HttpResponseRedirect('/')
else:
form = UserForm()
return render_to_response(
'register.html',
{
'form': form,
'months': range(1, 12),
'publishable': settings.STRIPE_PUBLISHABLE,
'soon': soon(),
'user': user,
'years': range(2011, 2036),
},
context_instance=RequestContext(request)
)
Den här vyn anropar först Customer.create
som faktiskt kallar Stripe för att hantera kreditkortshanteringen. Sedan skapar vi en ny användare. Om vi fick ett svar från Stripe uppdaterar vi den nyskapade kunden med stripe_id
. Om vi inte får tillbaka en kund (Stripe är nere) kommer vi att lägga till en post i UnpaidUsers
tabell med den nyskapade kundernas e-post, så att vi kan be dem att försöka igen med sina kreditkortsuppgifter senare.
Tanken är att även om Stripe är nere kan användaren fortfarande registrera sig och börja använda vår sida. Vi kommer bara att fråga dem igen vid ett senare tillfälle om kreditkortsinformation.
Jag förstår att det här kan vara ett lite konstruerat exempel, och det är inte så jag skulle implementera sådan funktionalitet om jag var tvungen, men syftet är att visa transaktioner.
Framåt. Tänker på transaktioner och tänker på att Django 1.6 som standard ger oss AUTOCOMMIT
beteende för vår databas, låt oss titta på den databasrelaterade koden lite längre.
cd = form.cleaned_data
try:
user = User.create(
cd['name'], cd['email'],
cd['password'], cd['last_4_digits'])
if customer:
user.stripe_id = customer.id
user.save()
else:
UnpaidUsers(email=cd['email']).save()
except IntegrityError:
# ...
Kan du upptäcka några problem? Tja, vad händer om UnpaidUsers(email=cd['email']).save()
raden misslyckas?
Du kommer att ha en användare, registrerad i systemet, som systemet tror har verifierat sitt kreditkort, men i själva verket har de inte verifierat kortet.
Vi vill bara ha ett av två resultat:
- Användaren skapas (i databasen) och har ett
stripe_id
. - Användaren skapas (i databasen) och har inget
stripe_id
OCH en associerad rad iUnpaidUsers
tabell med samma e-postadress genereras.
Vilket betyder att vi vill att de två separata databassatserna antingen båda commit eller båda återställs. Ett perfekt fodral för den ödmjuka transaktionen.
Låt oss först skriva några tester för att verifiera att saker beter sig som vi vill att de ska göra.
@mock.patch('payments.models.UnpaidUsers.save', side_effect = IntegrityError)
def test_registering_user_when_strip_is_down_all_or_nothing(self, save_mock):
#create the request used to test the view
self.request.session = {}
self.request.method='POST'
self.request.POST = {'email' : '[email protected]',
'name' : 'pyRock',
'stripe_token' : '...',
'last_4_digits' : '4242',
'password' : 'bad_password',
'ver_password' : 'bad_password',
}
#mock out stripe and ask it to throw a connection error
with mock.patch('stripe.Customer.create', side_effect =
socket.error("can't connect to stripe")) as stripe_mock:
#run the test
resp = register(self.request)
#assert there is no record in the database without stripe id.
users = User.objects.filter(email="[email protected]")
self.assertEquals(len(users), 0)
#check the associated table also didn't get updated
unpaid = UnpaidUsers.objects.filter(email="[email protected]")
self.assertEquals(len(unpaid), 0)
Dekoratören högst upp i testet är en låtsas som kommer att kasta ett "IntegrityError" när vi försöker spara till UnpaidUsers
bord.
Detta är för att svara på frågan, "Vad händer om UnpaidUsers(email=cd['email']).save()
linjen misslyckas?" Nästa kodbit skapar bara en hånad session, med lämplig information vi behöver för vår registreringsfunktion. Och sedan with mock.patch
tvingar systemet att tro att Stripe är nere ... äntligen kommer vi till testet.
resp = register(self.request)
Ovanstående rad anropar bara vår registervyfunktion som skickar den hånade begäran. Sedan kontrollerar vi bara att tabellerna inte är uppdaterade:
#assert there is no record in the database without stripe_id.
users = User.objects.filter(email="[email protected]")
self.assertEquals(len(users), 0)
#check the associated table also didn't get updated
unpaid = UnpaidUsers.objects.filter(email="[email protected]")
self.assertEquals(len(unpaid), 0)
Så det borde misslyckas om vi kör testet:
======================================================================
FAIL: test_registering_user_when_strip_is_down_all_or_nothing (tests.payments.testViews.RegisterPageTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/j1z0/.virtualenvs/django_1.6/lib/python2.7/site-packages/mock.py", line 1201, in patched
return func(*args, **keywargs)
File "/Users/j1z0/Code/RealPython/mvp_for_Adv_Python_Web_Book/tests/payments/testViews.py", line 266, in test_registering_user_when_strip_is_down_all_or_nothing
self.assertEquals(len(users), 0)
AssertionError: 1 != 0
----------------------------------------------------------------------
Trevlig. Verkar roligt att säga men det var precis vad vi ville. Kom ihåg:vi tränar TDD här. Felmeddelandet talar om för oss att användaren verkligen lagras i databasen - vilket är precis vad vi inte vill ha eftersom de inte betalade!
Transaktioner till undsättning …
Transaktioner
Det finns faktiskt flera sätt att skapa transaktioner i Django 1.6.
Låt oss gå igenom några.
Det rekommenderade sättet
Enligt Django 1.6 dokumentation:
"Django tillhandahåller ett enda API för att kontrollera databastransaktioner. […] Atomicitet är den definierande egenskapen för databastransaktioner. atomic tillåter oss att skapa ett kodblock inom vilket atomiciteten i databasen är garanterad. Om kodblocket slutförs framgångsrikt, överförs ändringarna till databasen. Om det finns ett undantag rullas ändringarna tillbaka.”
Atomic kan användas både som dekoratör eller som context_manager. Så om vi använder det som en kontexthanterare skulle koden i vår registerfunktion se ut så här:
from django.db import transaction
try:
with transaction.atomic():
user = User.create(
cd['name'], cd['email'],
cd['password'], cd['last_4_digits'])
if customer:
user.stripe_id = customer.id
user.save()
else:
UnpaidUsers(email=cd['email']).save()
except IntegrityError:
form.addError(cd['email'] + ' is already a member')
Notera raden with transaction.atomic()
. All kod i det blocket kommer att exekveras i en transaktion. Så om vi kör om våra tester borde de alla bli godkända! Kom ihåg att en transaktion är en enda arbetsenhet, så allt i kontexthanteraren rullas tillbaka när UnpaidUsers
samtalet misslyckas.
Använda en dekoratör
Vi kan också försöka lägga till atomic som dekoratör.
@transaction.atomic():
def register(request):
# ...snip....
try:
user = User.create(
cd['name'], cd['email'],
cd['password'], cd['last_4_digits'])
if customer:
user.stripe_id = customer.id
user.save()
else:
UnpaidUsers(email=cd['email']).save()
except IntegrityError:
form.addError(cd['email'] + ' is already a member')
Om vi kör om våra tester kommer de att misslyckas med samma fel som vi hade tidigare.
Varför är det så? Varför återgick inte transaktionen korrekt? Anledningen är att transaction.atomic
letar efter något slags undantag och vi fångade det felet (dvs. IntegrityError
i vårt försök utom block), så transaction.atomic
såg den aldrig och därmed standarden AUTOCOMMIT
funktionalitet tog över.
Men om du tar bort försöket undantag kommer naturligtvis undantaget att bara kastas upp i samtalskedjan och med största sannolikhet sprängas någon annanstans. Så det kan vi inte göra heller.
Så tricket är att placera den atomära kontexthanteraren i försöket utom blocket vilket är vad vi gjorde i vår första lösning. Tittar på rätt kod igen:
from django.db import transaction
try:
with transaction.atomic():
user = User.create(
cd['name'], cd['email'],
cd['password'], cd['last_4_digits'])
if customer:
user.stripe_id = customer.id
user.save()
else:
UnpaidUsers(email=cd['email']).save()
except IntegrityError:
form.addError(cd['email'] + ' is already a member')
När UnpaidUsers
aktiverar IntegrityError
transaction.atomic()
Context Manager kommer att fånga det och utföra återställningen. När vår kod körs i undantagshanteraren, (dvs. form.addError
linje) kommer återställningen att göras och vi kan säkert göra databasanrop om det behövs. Notera också eventuella databasanrop före eller efter transaction.atomic()
context manager kommer att påverkas oberoende av det slutliga resultatet av context_manager.
Transaktion per HTTP-förfrågan
Django 1.6 (som 1.5) låter dig också arbeta i ett "Transaktion per begäran"-läge. I detta läge kommer Django automatiskt att slå in din visningsfunktion i en transaktion. Om funktionen ger ett undantag kommer Django att återställa transaktionen, annars kommer den att utföra transaktionen.
För att få det konfigurerat måste du ställa in ATOMIC_REQUEST
till True i databaskonfigurationen för varje databas som du vill ska ha detta beteende. Så i vår "settings.py" gör vi ändringen så här:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(SITE_ROOT, 'test.db'),
'ATOMIC_REQUEST': True,
}
}
I praktiken fungerar detta precis som om du sätter dekoratören på vår utsiktsfunktion. Så det tjänar inte våra syften här.
Det är dock värt att notera att med båda ATOMIC_REQUESTS
och @transaction.atomic
dekoratör är det möjligt att fortfarande fånga / hantera dessa fel efter att de kastats från sikten. För att fånga dessa fel måste du implementera någon anpassad mellanprogramvara, eller så kan du åsidosätta urls.hadler500 eller genom att skapa en 500.html-mall.
SavePoints
Även om transaktioner är atomära kan de delas upp ytterligare i räddningspunkter. Tänk på sparpunkter som deltransaktioner.
Så om du har en transaktion som tar fyra SQL-satser att slutföra, kan du skapa en räddningspunkt efter den andra satsen. När den räddningspunkten väl har skapats, även om den 3:e eller 4:e satsen misslyckas, kan du göra en partiell återställning, bli av med den 3:e och 4:e satsen men behålla de två första.
Så det är i princip som att dela upp en transaktion i mindre lättviktstransaktioner så att du kan göra partiella återbetalningar eller åtaganden.
Men tänk på om huvudtransaktionen ska återställas (kanske på grund av en
IntegrityError
som höjdes och inte fångades, då kommer alla räddningspoäng också att rullas tillbaka).
Låt oss titta på ett exempel på hur räddningspunkter fungerar.
@transaction.atomic()
def save_points(self,save=True):
user = User.create('jj','inception','jj','1234')
sp1 = transaction.savepoint()
user.name = 'starting down the rabbit hole'
user.stripe_id = 4
user.save()
if save:
transaction.savepoint_commit(sp1)
else:
transaction.savepoint_rollback(sp1)
Här är hela funktionen i en transaktion. Efter att ha skapat en ny användare skapar vi en räddningspunkt och får en referens till räddningspunkten. De följande tre påståendena-
user.name = 'starting down the rabbit hole'
user.stripe_id = 4
user.save()
-är inte en del av den befintliga sparpunkten, så de har chansen att vara en del av nästa savepoint_rollback
, eller savepoint_commit
. I fallet med en savepoint_rollback
, raden user = User.create('jj','inception','jj','1234')
kommer fortfarande att vara engagerad i databasen även om resten av uppdateringarna inte gör det.
Med andra ord beskriver dessa följande två tester hur räddningspunkterna fungerar:
def test_savepoint_rollbacks(self):
self.save_points(False)
#verify that everything was stored
users = User.objects.filter(email="inception")
self.assertEquals(len(users), 1)
#note the values here are from the original create call
self.assertEquals(users[0].stripe_id, '')
self.assertEquals(users[0].name, 'jj')
def test_savepoint_commit(self):
self.save_points(True)
#verify that everything was stored
users = User.objects.filter(email="inception")
self.assertEquals(len(users), 1)
#note the values here are from the update calls
self.assertEquals(users[0].stripe_id, '4')
self.assertEquals(users[0].name, 'starting down the rabbit hole')
Även efter att vi har bekräftat eller återställt en räddningspunkt kan vi fortsätta att arbeta i samma transaktion. Och det arbetet kommer att påverkas av resultatet av den tidigare räddningspunkten.
Till exempel om vi uppdaterar våra save_points
fungerar som sådan:
@transaction.atomic()
def save_points(self,save=True):
user = User.create('jj','inception','jj','1234')
sp1 = transaction.savepoint()
user.name = 'starting down the rabbit hole'
user.save()
user.stripe_id = 4
user.save()
if save:
transaction.savepoint_commit(sp1)
else:
transaction.savepoint_rollback(sp1)
user.create('limbo','illbehere@forever','mind blown',
'1111')
Oavsett om savepoint_commit
eller savepoint_rollback
kallades "limbo"-användaren kommer fortfarande att skapas framgångsrikt. Om inte något annat gör att hela transaktionen återställs.
Inkapslade transaktioner
Förutom att manuellt ange sparpunkter, med savepoint()
, savepoint_commit
och savepoint_rollback
, skapar en kapslad transaktion automatiskt en räddningspunkt för oss och återställer den om vi får ett felmeddelande.
Om vi utökar vårt exempel lite längre får vi:
@transaction.atomic()
def save_points(self,save=True):
user = User.create('jj','inception','jj','1234')
sp1 = transaction.savepoint()
user.name = 'starting down the rabbit hole'
user.save()
user.stripe_id = 4
user.save()
if save:
transaction.savepoint_commit(sp1)
else:
transaction.savepoint_rollback(sp1)
try:
with transaction.atomic():
user.create('limbo','illbehere@forever','mind blown',
'1111')
if not save: raise DatabaseError
except DatabaseError:
pass
Här kan vi se att efter att vi har hanterat våra räddningspunkter använder vi transaction.atomic
kontexthanterare för att omsluta vårt skapande av "limbo"-användaren. När den kontexthanteraren anropas skapar den i själva verket en räddningspunkt (eftersom vi redan är i en transaktion) och den räddningspunkten kommer att committeras eller återställas när kontexthanteraren avslutas.
Följande två tester beskriver alltså deras beteende:
def test_savepoint_rollbacks(self):
self.save_points(False)
#verify that everything was stored
users = User.objects.filter(email="inception")
self.assertEquals(len(users), 1)
#savepoint was rolled back so we should have original values
self.assertEquals(users[0].stripe_id, '')
self.assertEquals(users[0].name, 'jj')
#this save point was rolled back because of DatabaseError
limbo = User.objects.filter(email="illbehere@forever")
self.assertEquals(len(limbo),0)
def test_savepoint_commit(self):
self.save_points(True)
#verify that everything was stored
users = User.objects.filter(email="inception")
self.assertEquals(len(users), 1)
#savepoint was committed
self.assertEquals(users[0].stripe_id, '4')
self.assertEquals(users[0].name, 'starting down the rabbit hole')
#save point was committed by exiting the context_manager without an exception
limbo = User.objects.filter(email="illbehere@forever")
self.assertEquals(len(limbo),1)
Så i verkligheten kan du använda antingen atomic
eller savepoint
för att skapa sparpunkter i en transaktion. Med atomic
du behöver inte uttryckligen oroa dig för commit / återställning, där som med savepoint
du har full kontroll över när det händer.
Slutsats
Om du hade någon tidigare erfarenhet av tidigare versioner av Django-transaktioner kan du se hur mycket enklare transaktionsmodellen är. Har även AUTOCOMMIT
on som standard är ett bra exempel på "sansade" standardinställningar som både Django och Python är stolta över att leverera. För många system behöver du inte hantera transaktioner direkt, låt bara AUTOCOMMIT
göra sitt arbete. Men om du gör det kommer förhoppningsvis detta inlägg att ha gett dig den information du behöver för att hantera transaktioner i Django som ett proffs.