Pokud jste někdy věnovali mnoho času správě transakcí databáze Django, víte, jak to může být matoucí. V minulosti poskytovala dokumentace poměrně velkou hloubku, ale pochopení přišlo pouze budováním a experimentováním.
Pracovalo se s velkým množstvím dekoratérů, jako je commit_on_success
, commit_manually
, commit_unless_managed
, rollback_unless_managed
, enter_transaction_management
, leave_transaction_management
, abychom jmenovali alespoň některé. Naštěstí s Django 1.6 jde všechno za dveře. Nyní opravdu potřebujete vědět pouze o několika funkcích. A k těm se za vteřinu dostaneme. Nejprve se budeme věnovat těmto tématům:
- Co je správa transakcí?
- Co je špatného na správě transakcí před verzí Django 1.6?
Před skokem do:
- Co je správné na správě transakcí v Django 1.6?
A pak se zabývám podrobným příkladem:
- Příklad proužků
- Transakce
- Doporučený způsob
- Použití dekoratér
- Transakce na požadavek HTTP
- Uložené body
- Vnořené transakce
Co je to transakce?
Podle SQL-92 je „SQL-transakce (někdy jednoduše nazývaná „transakce“) posloupnost provádění příkazů SQL, která je atomická s ohledem na obnovu“. Jinými slovy, všechny příkazy SQL jsou provedeny a potvrzeny společně. Podobně, když se vrátíte zpět, všechny příkazy se vrátí dohromady.
Například:
# START
note = Note(title="my first note", text="Yay!")
note = Note(title="my second note", text="Whee!")
address1.save()
address2.save()
# COMMIT
Transakce je tedy jedna jednotka práce v databázi. A tato jediná jednotka práce je vymezena počáteční transakcí a poté potvrzením nebo explicitním vrácením zpět.
Co je špatného na správě transakcí před verzí Django 1.6?
Abychom mohli plně odpovědět na tuto otázku, musíme se zabývat tím, jak jsou transakce řešeny v databázi, klientských knihovnách a v rámci Django.
Databáze
Každý příkaz v databázi musí běžet v transakci, i když transakce obsahuje pouze jeden příkaz.
Většina databází má AUTOCOMMIT
nastavení, které je obvykle jako výchozí nastaveno na hodnotu True. Tento AUTOCOMMIT
zabalí každý příkaz do transakce, která je okamžitě potvrzena, pokud je příkaz úspěšný. Samozřejmě můžete ručně zavolat něco jako START_TRANSACTION
což dočasně pozastaví AUTOCOMMIT
dokud nezavoláte COMMIT_TRANSACTION
nebo ROLLBACK
.
Výhodou je však to, že AUTOCOMMIT
nastavení použije implicitní potvrzení po každém příkazu .
Klientské knihovny
Pak jsou tu klientské knihovny Pythonu jako sqlite3 a mysqldb, které umožňují programům Pythonu propojit se se samotnými databázemi. Takové knihovny se řídí sadou standardů, jak přistupovat k databázím a dotazovat se na ně. Tento standard, DB API 2.0, je popsán v PEP 249. I když to může vést k poněkud suchému čtení, důležité je, že PEP 249 uvádí, že databáze AUTOCOMMIT
by mělo být VYPNUTO ve výchozím nastavení.
To je jasně v rozporu s tím, co se děje v databázi:
- Příkazy SQL se musí vždy spouštět v transakci, kterou vám databáze obvykle otevře pomocí
AUTOCOMMIT
. - To by se však podle PEP 249 nemělo stávat.
- Klientské knihovny musí zrcadlit to, co se děje v databázi, ale protože jim není povoleno zapnout
AUTOCOMMIT
ve výchozím nastavení jednoduše zabalí vaše příkazy SQL do transakce, stejně jako databáze.
Dobře. Zůstaň se mnou ještě chvíli.
Django
Vstupte Django. Django má také co říci k řízení transakcí. V Django 1.5 a dřívějších verzích Django v podstatě běžel s otevřenou transakcí a tuto transakci automaticky svěřil, když jste zapisovali data do databáze. Takže pokaždé, když jste zavolali něco jako model.save()
nebo model.update()
Django vygeneroval příslušné příkazy SQL a provedl transakci.
Také v Django 1.5 a dřívějších verzích bylo doporučeno použít TransactionMiddleware
k navázání transakcí na požadavky HTTP. Ke každé žádosti byla zadána transakce. Pokud se odpověď vrátí bez výjimek, Django transakci potvrdí, ale pokud vaše funkce zobrazení vyhodí chybu, ROLLBACK
by se volalo. To ve skutečnosti vypnulo AUTOCOMMIT
. Pokud jste chtěli standardní správu transakcí ve stylu autocommit na úrovni databáze, museli jste transakce spravovat sami – obvykle pomocí dekorátoru transakcí ve funkci zobrazení, jako je @transaction.commit_manually
nebo @transaction.commit_on_success
.
Nadechni se. Nebo dva.
Co to znamená?
Ano, děje se toho hodně a ukázalo se, že většina vývojářů chce pouze standardní automatické zavazování na úrovni databáze – což znamená, že transakce zůstávají za scénou a dělají svou věc, dokud je nebudete muset ručně upravit.
Co je správné na správě transakcí v Django 1.6?
Nyní vítejte v Django 1.6. Snažte se zapomenout na vše, o čem jsme právě mluvili, a jednoduše si pamatujte, že v Django 1.6 používáte databázi AUTOCOMMIT
a v případě potřeby spravovat transakce ručně. V podstatě máme mnohem jednodušší model, který v podstatě dělá to, k čemu byla databáze navržena.
Dost teorie. Pojďme kódovat.
Příklad proužků
Zde máme tuto ukázkovou funkci zobrazení, která se zabývá registrací uživatele a voláním Stripe za účelem zpracování kreditní karty.
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)
)
Toto zobrazení nejprve volá Customer.create
který ve skutečnosti volá Stripe, aby se postaral o zpracování kreditní karty. Poté vytvoříme nového uživatele. Pokud dostaneme odpověď od Stripe, aktualizujeme nově vytvořeného zákazníka pomocí stripe_id
. Pokud nezískáme zákazníka zpět (Stripe nefunguje), přidáme záznam do UnpaidUsers
tabulky s nově vytvořeným e-mailem zákazníků, abychom je mohli požádat, aby znovu zkusili údaje o své kreditní kartě později.
Myšlenka je taková, že i když Stripe nefunguje, uživatel se stále může zaregistrovat a začít používat naše stránky. Jen je později znovu požádáme o informace o kreditní kartě.
Chápu, že to může být trochu vymyšlený příklad a není to způsob, jakým bych takovou funkci implementoval, kdybych musel, ale účelem je demonstrovat transakce.
Kupředu. Přemýšlejte o transakcích a mějte na paměti, že ve výchozím nastavení nám Django 1.6 poskytuje AUTOCOMMIT
chování naší databáze, podívejme se na kód související s databází trochu déle.
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:
# ...
Dokážete najít nějaké problémy? Co se stane, když UnpaidUsers(email=cd['email']).save()
linka selhává?
Budete mít uživatele registrovaného v systému, o kterém si systém myslí, že ověřil jeho kreditní kartu, ale ve skutečnosti kartu neověřil.
Chceme pouze jeden ze dvou výsledků:
- Uživatel je vytvořen (v databázi) a má
stripe_id
. - Uživatel je vytvořen (v databázi) a nemá
stripe_id
A přidružený řádek vUnpaidUsers
se vygeneruje tabulka se stejnou e-mailovou adresou.
Což znamená, že chceme, aby dva samostatné databázové příkazy byly buď potvrzeny, nebo byly oba odvolány. Perfektní případ pro skromné transakce.
Nejprve si napišme nějaké testy, abychom ověřili, že se věci chovají tak, jak chceme.
@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)
Dekorátor v horní části testu je model, který při pokusu o uložení do UnpaidUsers
vyvolá chybu „IntegrityError“. tabulka.
Toto je odpověď na otázku:„Co se stane, když UnpaidUsers(email=cd['email']).save()
linka selhává?" Další kousek kódu pouze vytvoří falešnou relaci s příslušnými informacemi, které potřebujeme pro naši registrační funkci. A pak with mock.patch
nutí systém věřit, že Stripe je mimo provoz… konečně se dostáváme k testu.
resp = register(self.request)
Výše uvedený řádek pouze zavolá naši funkci zobrazení registru předáním falešného požadavku. Pak jen zkontrolujeme, zda nejsou tabulky aktualizovány:
#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)
Takže by to mělo selhat, pokud spustíme test:
======================================================================
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
----------------------------------------------------------------------
Pěkný. Zní to legračně, ale to je přesně to, co jsme chtěli. Nezapomeňte:zde procvičujeme TDD. Chybová zpráva nám říká, že uživatel je skutečně uložen v databázi – což je přesně to, co nechceme, protože nezaplatil!
Transakce na záchranu …
Transakce
Ve skutečnosti existuje několik způsobů, jak vytvořit transakce v Django 1.6.
Pojďme si jich několik projít.
Doporučený způsob
Podle dokumentace Django 1.6:
„Django poskytuje jediné API pro řízení databázových transakcí. […] Atomicita je určující vlastností databázových transakcí. atomic nám umožňuje vytvořit blok kódu, ve kterém je zaručena atomičnost v databázi. Pokud je blok kódu úspěšně dokončen, změny se uloží do databáze. Pokud existuje výjimka, změny jsou vráceny zpět.“
Atomic lze použít jako dekorátor nebo jako context_manager. Pokud jej tedy použijeme jako správce kontextu, bude kód v naší funkci registru vypadat takto:
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')
Všimněte si řádku with transaction.atomic()
. Veškerý kód v tomto bloku bude proveden v rámci transakce. Takže pokud znovu spustíme naše testy, měly by všechny projít! Pamatujte, že transakce je jedna jednotka práce, takže vše uvnitř kontextového manažera se vrátí dohromady, když UnpaidUsers
hovor se nezdaří.
Použití dekorátoru
Můžeme také zkusit přidat atomic jako dekoraci.
@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')
Pokud znovu spustíme naše testy, selžou se stejnou chybou, jakou jsme měli předtím.
proč tomu tak je? Proč se transakce nevrátila správně? Důvodem je, že transaction.atomic
hledá nějakou výjimku a dobře, zachytili jsme tuto chybu (tj. IntegrityError
v našem pokusu kromě bloku), takže transaction.atomic
nikdy neviděl a tedy standardní AUTOCOMMIT
funkce převzala.
Ale samozřejmě odstranění pokusu s výjimkou způsobí, že výjimka bude prostě vyhozena do řetězce volání a pravděpodobně vybuchne někde jinde. Takže to nemůžeme udělat ani my.
Takže trik je vložit správce atomických kontextů dovnitř bloku try, což je to, co jsme udělali v našem prvním řešení. Znovu se podíváte na správný kód:
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')
Když UnpaidUsers
spustí IntegrityError
transaction.atomic()
kontextový manažer to zachytí a provede vrácení zpět. V době, kdy se náš kód spustí v obslužné rutině výjimky, (tj. form.addError
linka) bude proveden návrat a v případě potřeby můžeme bezpečně provést volání databáze. Poznamenejte si také všechna databázová volání před nebo po transaction.atomic()
kontextový manažer nebude ovlivněn bez ohledu na konečný výsledek context_manager.
Transakce na požadavek HTTP
Django 1.6 (jako 1.5) vám také umožňuje pracovat v režimu „Transakce na žádost“. V tomto režimu Django automaticky zabalí vaši funkci zobrazení do transakce. Pokud funkce vyvolá výjimku, Django vrátí transakci zpět, jinak transakci potvrdí.
Chcete-li jej nastavit, musíte nastavit ATOMIC_REQUEST
na hodnotu True v konfiguraci databáze pro každou databázi, u které chcete mít toto chování. Takže v našem „settings.py“ provedeme změnu takto:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(SITE_ROOT, 'test.db'),
'ATOMIC_REQUEST': True,
}
}
V praxi se to chová přesně tak, jako kdybyste umístili dekoratér na naši funkci zobrazení. Zde tedy neslouží našim účelům.
Je však užitečné poznamenat, že s oběma ATOMIC_REQUESTS
a @transaction.atomic
dekoratér je možné tyto chyby zachytit/ošetřit i poté, co jsou shozeny z pohledu. Abyste tyto chyby zachytili, museli byste implementovat nějaký vlastní middleware, nebo byste mohli přepsat urls.hadler500 nebo vytvořit šablonu 500.html.
Uložené body
I když jsou transakce atomické, lze je dále rozdělit na body uložení. Uvažujte o uložených bodech jako o dílčích transakcích.
Pokud tedy máte transakci, která vyžaduje čtyři příkazy SQL k dokončení, můžete po druhém příkazu vytvořit bod uložení. Jakmile je tento záchranný bod vytvořen, i když selže 3. nebo 4. příkaz, můžete provést částečný návrat a zbavit se 3. a 4. příkazu, ale zachovat první dva.
Je to tedy v podstatě jako rozdělení transakce na menší odlehčené transakce, které vám umožní provádět částečná vrácení zpět nebo potvrzení.
Ale mějte na paměti, pokud je hlavní transakce, kde se má dostat, vrácena zpět (možná kvůli
IntegrityError
která byla zvednuta a nebyla zachycena, pak se všechny body uložení také vrátí zpět).
Podívejme se na příklad toho, jak fungují body uložení.
@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)
Zde je celá funkce v transakci. Po vytvoření nového uživatele vytvoříme bod uložení a získáme odkaz na bod uložení. Další tři prohlášení-
user.name = 'starting down the rabbit hole'
user.stripe_id = 4
user.save()
-nejsou součástí existujícího bodu uložení, takže mají šanci být součástí dalšího savepoint_rollback
nebo savepoint_commit
. V případě savepoint_rollback
, řádek user = User.create('jj','inception','jj','1234')
bude stále vázána na databázi, i když zbytek aktualizací nikoli.
Jinak řečeno, tyto dva testy popisují, jak fungují body uložení:
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')
Také poté, co potvrdíme nebo vrátíme zpět bod uložení, můžeme pokračovat v práci ve stejné transakci. A tato práce nebude ovlivněna výsledkem předchozího bodu uložení.
Pokud například aktualizujeme naše save_points
funkce jako taková:
@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')
Bez ohledu na to, zda savepoint_commit
nebo savepoint_rollback
byl nazván „limbo“ uživatel bude stále úspěšně vytvořen. Pokud něco jiného nezpůsobí vrácení celé transakce.
Vnořené transakce
Kromě ručního zadávání bodů uložení pomocí savepoint()
, savepoint_commit
a savepoint_rollback
, vytvoření vnořené transakce nám automaticky vytvoří bod uložení a vrátí jej zpět, pokud se objeví chyba.
Rozšířením našeho příkladu o něco dále dostaneme:
@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
Zde vidíme, že poté, co se vypořádáme s našimi body uložení, používáme transaction.atomic
kontextový manažer, který zapouzdří naše vytvoření uživatele „limbo“. Když je zavolán tento kontextový manažer, ve skutečnosti se vytváří bod uložení (protože jsme již v transakci) a tento bod uložení bude potvrzen nebo vrácen zpět po ukončení správce kontextu.
Následující dva testy tedy popisují jejich chování:
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)
Takže ve skutečnosti můžete použít buď atomic
nebo savepoint
k vytvoření bodů uložení uvnitř transakce. S atomic
nemusíte se explicitně starat o potvrzení / vrácení zpět, kde jako u savepoint
máte plnou kontrolu nad tím, kdy se to stane.
Závěr
Pokud jste měli nějaké předchozí zkušenosti s dřívějšími verzemi transakcí Django, můžete vidět, o kolik jednodušší je transakční model. Také s funkcí AUTOCOMMIT
zapnuto ve výchozím nastavení je skvělým příkladem „rozumných“ výchozích nastavení, na jejichž poskytování se Django i Python pyšní. U mnoha systémů nebudete muset transakce přímo řešit, stačí nechat AUTOCOMMIT
dělat svou práci. Ale pokud to uděláte, doufejme, že vám tento příspěvek poskytne informace, které potřebujete ke správě transakcí v Django jako profesionál.