Jeśli kiedykolwiek poświęciłeś dużo czasu na zarządzanie transakcjami w bazie danych Django, wiesz, jak bardzo może to być zagmatwane. W przeszłości dokumentacja zapewniała sporo głębi, ale zrozumienie można było uzyskać tylko poprzez budowanie i eksperymentowanie.
Było mnóstwo dekoratorów do pracy, takich jak commit_on_success
, commit_manually
, commit_unless_managed
, rollback_unless_managed
, enter_transaction_management
, leave_transaction_management
, żeby wymienić tylko kilka. Na szczęście z Django 1.6 wszystko wychodzi poza drzwi. Teraz naprawdę musisz wiedzieć tylko o kilku funkcjach. A do nich dojdziemy za chwilę. Najpierw zajmiemy się tymi tematami:
- Co to jest zarządzanie transakcjami?
- Co jest nie tak z zarządzaniem transakcjami przed Django 1.6?
Przed przejściem do:
- Co jest dobrego w zarządzaniu transakcjami w Django 1.6?
A potem zajmijmy się szczegółowym przykładem:
- Przykład paska
- Transakcje
- Zalecany sposób
- Korzystanie z dekoratora
- Transakcja na żądanie HTTP
- Zapisz punkty
- Zagnieżdżone transakcje
Co to jest transakcja?
Zgodnie z SQL-92 „Transakcja SQL (czasami nazywana po prostu „transakcją”) to sekwencja wykonań instrukcji SQL, która jest niepodzielna w odniesieniu do odzyskiwania”. Innymi słowy, wszystkie instrukcje SQL są wykonywane i zatwierdzane razem. Podobnie po wycofaniu wszystkie stwierdzenia są wycofywane razem.
Na przykład:
# START
note = Note(title="my first note", text="Yay!")
note = Note(title="my second note", text="Whee!")
address1.save()
address2.save()
# COMMIT
Tak więc transakcja to pojedyncza jednostka pracy w bazie danych. A ta pojedyncza jednostka pracy jest odgraniczona przez transakcję początkową, a następnie zatwierdzenie lub jawne wycofanie.
Co jest nie tak z zarządzaniem transakcjami przed Django 1.6?
Aby w pełni odpowiedzieć na to pytanie, musimy zająć się sposobem obsługi transakcji w bazie danych, bibliotekach klienta i w Django.
Bazy danych
Każdy wyciąg w bazie danych musi zostać uruchomiony w transakcji, nawet jeśli transakcja zawiera tylko jeden wyciąg.
Większość baz danych ma funkcję AUTOCOMMIT
ustawienie, które jest zwykle ustawione na True jako domyślne. To AUTOCOMMIT
otacza każdą instrukcję w transakcji, która jest natychmiast zatwierdzana, jeśli instrukcja się powiedzie. Oczywiście możesz ręcznie wywołać coś takiego jak START_TRANSACTION
co tymczasowo zawiesi AUTOCOMMIT
dopóki nie zadzwonisz do COMMIT_TRANSACTION
lub ROLLBACK
.
Jednakże wniosek jest taki, że AUTOCOMMIT
ustawienie stosuje niejawne zatwierdzenie po każdej instrukcji .
Biblioteki klienta
Są też biblioteki klienckie Pythona jak sqlite3 i mysqldb, które umożliwiają programom Pythona łączenie się z samymi bazami danych. Takie biblioteki przestrzegają zestawu standardów dotyczących dostępu do baz danych i wykonywania zapytań. Ten standard, DB API 2.0, jest opisany w PEP 249. Chociaż może to sprawić, że czytanie może być nieco suche, ważnym wnioskiem jest to, że PEP 249 stwierdza, że baza danych AUTOCOMMIT
powinna być WYŁĄCZONA domyślnie.
To wyraźnie koliduje z tym, co dzieje się w bazie danych:
- Wyrażenia SQL zawsze muszą być uruchamiane w transakcji, którą baza danych zazwyczaj otwiera dla Ciebie za pomocą funkcji
AUTOCOMMIT
. - Jednak według PEP 249 nie powinno to mieć miejsca.
- Biblioteki klienta muszą odzwierciedlać to, co dzieje się w bazie danych, ale ponieważ nie mogą włączać funkcji
AUTOCOMMIT
domyślnie, po prostu zawijają twoje instrukcje SQL w transakcję, tak jak baza danych.
Dobra. Zostań ze mną trochę dłużej.
Django
Wejdź do Django. Django ma też coś do powiedzenia na temat zarządzania transakcjami. W Django 1.5 i wcześniejszych, Django zasadniczo działał z otwartą transakcją i automatycznie zatwierdzał tę transakcję, gdy zapisywałeś dane do bazy danych. Więc za każdym razem, gdy wywołałeś coś takiego jak model.save()
lub model.update()
, Django wygenerowało odpowiednie instrukcje SQL i zatwierdziło transakcję.
Również w Django 1.5 i wcześniejszych zalecano użycie TransactionMiddleware
powiązania transakcji z żądaniami HTTP. Każde żądanie otrzymało transakcję. Jeśli odpowiedź zostanie zwrócona bez wyjątków, Django zatwierdzi transakcję, ale jeśli twoja funkcja widoku zwróci błąd, ROLLBACK
zostanie nazwany. W efekcie wyłączono funkcję AUTOCOMMIT
. Jeśli chciałeś standardowego zarządzania transakcjami w stylu automatycznego zatwierdzania na poziomie bazy danych, musiałeś samodzielnie zarządzać transakcjami - zwykle za pomocą dekoratora transakcji w funkcji widoku, takiego jak @transaction.commit_manually
lub @transaction.commit_on_success
.
Wziąć oddech. Albo dwa.
Co to oznacza?
Tak, dużo się tam dzieje i okazuje się, że większość programistów chce tylko standardowych automatycznych zatwierdzeń na poziomie bazy danych – co oznacza, że transakcje pozostają w ukryciu, robiąc swoje, dopóki nie będziesz musiał ich ręcznie dostosować.
Co jest dobrego w zarządzaniu transakcjami w Django 1.6?
Witamy w Django 1.6. Postaraj się zapomnieć o wszystkim, o czym właśnie rozmawialiśmy, i po prostu pamiętaj, że w Django 1.6 używasz bazy danych AUTOCOMMIT
i w razie potrzeby zarządzaj transakcjami ręcznie. Zasadniczo mamy znacznie prostszy model, który zasadniczo robi to, do czego baza danych została zaprojektowana w pierwszej kolejności.
Dość teorii. Zróbmy kod.
Przykład paska
Tutaj mamy tę przykładową funkcję widoku, która obsługuje rejestrację użytkownika i wywołanie Stripe w celu przetworzenia karty kredytowej.
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)
)
Ten widok najpierw wywołuje Customer.create
który faktycznie wywołuje Stripe do obsługi przetwarzania kart kredytowych. Następnie tworzymy nowego użytkownika. Jeśli otrzymaliśmy odpowiedź od Stripe, aktualizujemy nowo utworzonego klienta za pomocą stripe_id
. Jeśli nie odzyskamy klienta (Stripe nie działa), dodamy wpis do UnpaidUsers
tabela z nowo utworzonym adresem e-mail klienta, dzięki czemu możemy poprosić ich o ponowne ponowną próbę danych karty kredytowej później.
Chodzi o to, że nawet jeśli Stripe nie działa, użytkownik nadal może się zarejestrować i zacząć korzystać z naszej strony. Po prostu poprosimy ich ponownie w późniejszym terminie o dane karty kredytowej.
Rozumiem, że może to być trochę wymyślony przykład i nie jest to sposób, w jaki zaimplementowałbym taką funkcjonalność, gdybym musiał, ale celem jest zademonstrowanie transakcji.
Naprzód. Myśląc o transakcjach i pamiętając, że domyślnie Django 1.6 daje nam AUTOCOMMIT
zachowanie dla naszej bazy danych, spójrzmy trochę dłużej na kod związany z bazą danych.
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:
# ...
Czy widzisz jakieś problemy? Co się stanie, jeśli UnpaidUsers(email=cd['email']).save()
linia nie działa?
Będziesz mieć użytkownika zarejestrowanego w systemie, który według systemu zweryfikował swoją kartę kredytową, ale w rzeczywistości nie zweryfikował karty.
Chcemy tylko jednego z dwóch wyników:
- Użytkownik jest tworzony (w bazie danych) i ma
stripe_id
. - Użytkownik został utworzony (w bazie danych) i nie ma
stripe_id
ORAZ powiązany wiersz wUnpaidUsers
generowana jest tabela z tym samym adresem e-mail.
Co oznacza, że chcemy, aby dwie oddzielne instrukcje bazy danych zarówno zatwierdzały, jak i wycofywały. Idealny przypadek na skromną transakcję.
Najpierw napiszmy kilka testów, aby sprawdzić, czy rzeczy zachowują się tak, jak chcemy.
@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)
Dekorator na górze testu to próba, która wyrzuci „IntegrityError”, gdy spróbujemy zapisać do UnpaidUsers
tabela.
Ma to na celu odpowiedź na pytanie:„Co się stanie, jeśli UnpaidUsers(email=cd['email']).save()
linia nie działa?” Następny fragment kodu po prostu tworzy symulowaną sesję z odpowiednimi informacjami, których potrzebujemy do naszej funkcji rejestracji. A potem with mock.patch
zmusza system do przekonania, że Stripe nie działa… w końcu przechodzimy do testu.
resp = register(self.request)
Powyższa linia po prostu wywołuje naszą funkcję widoku rejestru przekazującą w zaklętym żądaniu. Następnie po prostu sprawdzamy, czy tabele nie są aktualizowane:
#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)
Więc powinno się nie udać, jeśli uruchomimy 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
----------------------------------------------------------------------
Ładny. Wydaje się śmieszne, ale to jest dokładnie to, czego chcieliśmy. Pamiętaj:ćwiczymy tutaj TDD. Komunikat o błędzie informuje nas, że użytkownik rzeczywiście jest przechowywany w bazie danych - a dokładnie tego nie chcemy, ponieważ nie zapłacił!
Transakcje na ratunek…
Transakcje
W Django 1.6 istnieje kilka sposobów tworzenia transakcji.
Omówmy kilka.
Zalecany sposób
Zgodnie z dokumentacją Django 1.6:
„Django zapewnia pojedyncze API do kontroli transakcji bazodanowych. […] Atomowość jest definiującą właściwością transakcji bazodanowych. atomic pozwala nam stworzyć blok kodu, w ramach którego gwarantowana jest niepodzielność bazy danych. Jeśli blok kodu zostanie pomyślnie zakończony, zmiany są zatwierdzane w bazie danych. Jeśli jest wyjątek, zmiany są cofane”.
Atomic może być używany zarówno jako dekorator, jak i jako menedżer_kontekstu. Więc jeśli użyjemy go jako menedżera kontekstu, kod w naszej funkcji rejestru wygląda tak:
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')
Zwróć uwagę na wiersz with transaction.atomic()
. Cały kod wewnątrz tego bloku zostanie wykonany w ramach transakcji. Jeśli więc ponownie uruchomimy nasze testy, wszystkie powinny zaliczyć! Pamiętaj, że transakcja to pojedyncza jednostka pracy, więc wszystko w menedżerze kontekstu zostaje wycofane razem, gdy UnpaidUsers
połączenie nie powiodło się.
Korzystanie z dekoratora
Możemy również spróbować dodać atomic jako dekorator.
@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')
Jeśli ponownie uruchomimy nasze testy, zakończą się niepowodzeniem z tym samym błędem, który mieliśmy wcześniej.
Dlaczego? Dlaczego transakcja nie została cofnięta poprawnie? Powodem jest to, że transaction.atomic
szuka jakiegoś wyjątku i cóż, wykryliśmy ten błąd (tj. IntegrityError
w naszej próbie z wyjątkiem bloku), więc transaction.atomic
nigdy tego nie widziałem, a zatem standardowe AUTOCOMMIT
funkcjonalność przejęła.
Ale oczywiście usunięcie try z wyjątkiem spowoduje, że wyjątek zostanie po prostu wyrzucony w górę łańcucha wywołań i najprawdopodobniej wybuchnie gdzie indziej. Więc my też nie możemy tego zrobić.
Sztuczka polega więc na umieszczeniu atomowego menedżera kontekstu w try z wyjątkiem bloku, który zrobiliśmy w naszym pierwszym rozwiązaniu. Patrząc ponownie na poprawny kod:
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')
Kiedy UnpaidUsers
uruchamia IntegrityError
transaction.atomic()
menedżer kontekstu przechwyci go i wykona wycofanie. Zanim nasz kod zostanie wykonany w procedurze obsługi wyjątków (tj. form.addError
line) wycofanie zostanie wykonane i w razie potrzeby będziemy mogli bezpiecznie wykonać wywołania bazy danych. Zwróć także uwagę na wszelkie wywołania bazy danych przed lub po transaction.atomic()
menedżer kontekstu nie będzie miał wpływu, niezależnie od końcowego wyniku menedżera_kontekstu.
Transakcja na żądanie HTTP
Django 1.6 (podobnie jak 1.5) pozwala również na działanie w trybie „Transakcja na żądanie”. W tym trybie Django automatycznie zapakuje Twoją funkcję widoku w transakcję. Jeśli funkcja zgłosi wyjątek, Django wycofa transakcję, w przeciwnym razie zatwierdzi transakcję.
Aby uzyskać konfigurację, musisz ustawić ATOMIC_REQUEST
na True w konfiguracji bazy danych dla każdej bazy danych, która ma mieć to zachowanie. Tak więc w naszym „settings.py” wprowadzamy taką zmianę:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(SITE_ROOT, 'test.db'),
'ATOMIC_REQUEST': True,
}
}
W praktyce zachowuje się to dokładnie tak, jakbyś umieścił dekorator w naszej funkcji widoku. Więc to nie służy naszym celom.
Warto jednak zauważyć, że w obu przypadkach ATOMIC_REQUESTS
i @transaction.atomic
dekoratorem nadal można wyłapywać/obsługiwać te błędy po ich wyrzuceniu z widoku. Aby wyłapać te błędy, musiałbyś zaimplementować niestandardowe oprogramowanie pośredniczące lub możesz nadpisać urls.hadler500 lub utworzyć szablon 500.html.
Zapisz punkty
Mimo że transakcje są atomowe, można je dalej podzielić na punkty zapisu. Pomyśl o punktach zapisu jako o transakcjach częściowych.
Jeśli więc masz transakcję, której wykonanie wymaga czterech instrukcji SQL, możesz utworzyć punkt zapisu po drugiej instrukcji. Po utworzeniu tego punktu zapisu, nawet jeśli trzecia lub czwarta instrukcja nie powiedzie się, możesz wykonać częściowe wycofanie, pozbywając się trzeciej i czwartej instrukcji, ale zachowując dwie pierwsze.
Więc to jest w zasadzie jak dzielenie transakcji na mniejsze, lekkie transakcje, co pozwala na częściowe wycofanie lub zatwierdzenie.
Pamiętaj jednak, że główna transakcja ma zostać wycofana (być może z powodu IntegrityError
który został podniesiony i nie został złapany, wszystkie savepointy również zostaną wycofane).
Spójrzmy na przykład, jak działają punkty zapisu.
@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)
Tutaj cała funkcja jest w transakcji. Po utworzeniu nowego użytkownika tworzymy punkt zapisu i otrzymujemy odniesienie do punktu zapisu. Kolejne trzy stwierdzenia-
user.name = 'starting down the rabbit hole'
user.stripe_id = 4
user.save()
-nie są częścią istniejącego punktu zapisu, więc mają szansę być częścią następnego savepoint_rollback
lub savepoint_commit
. W przypadku savepoint_rollback
, wiersz user = User.create('jj','inception','jj','1234')
nadal będą zapisywane w bazie danych, mimo że pozostałe aktualizacje nie.
Innymi słowy, poniższe dwa testy opisują działanie punktów zapisu:
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')
Również po zatwierdzeniu lub wycofaniu punktu zapisu możemy kontynuować pracę w tej samej transakcji. A na tę pracę nie będzie miał wpływu wynik poprzedniego punktu zapisu.
Na przykład, jeśli zaktualizujemy nasze save_points
funkcja jako taka:
@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')
Niezależnie od tego, czy savepoint_commit
lub savepoint_rollback
został nazwany „limbo”, użytkownik nadal zostanie pomyślnie utworzony. Chyba że coś innego spowoduje wycofanie całej transakcji.
Zagnieżdżone transakcje
Oprócz ręcznego określania punktów zapisu, za pomocą savepoint()
, savepoint_commit
i savepoint_rollback
, utworzenie zagnieżdżonej Transakcji automatycznie utworzy dla nas punkt zapisu i wycofa go, jeśli wystąpi błąd.
Rozszerzając nasz przykład nieco dalej, otrzymujemy:
@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
Tutaj widzimy, że po zajęciu się naszymi punktami zapisu używamy transaction.atomic
menedżer kontekstu, aby objąć nasze stworzenie użytkownika „limbo”. Gdy ten menedżer kontekstu jest wywoływany, w efekcie tworzy punkt zapisu (ponieważ jesteśmy już w transakcji) i ten punkt zapisu zostanie zatwierdzony lub wycofany po wyjściu z menedżera kontekstu.
Dlatego następujące dwa testy opisują ich zachowanie:
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 więc w rzeczywistości możesz użyć atomic
lub savepoint
do tworzenia punktów zapisu wewnątrz transakcji. Z atomic
nie musisz martwić się wyraźnie o zatwierdzenie / wycofanie, gdzie tak jak w przypadku savepoint
masz pełną kontrolę nad tym, kiedy to się dzieje.
Wniosek
Jeśli miałeś jakieś doświadczenia z wcześniejszymi wersjami transakcji Django, możesz zobaczyć, o ile prostszy jest model transakcji. Posiada również funkcję AUTOCOMMIT
on domyślnie jest doskonałym przykładem „rozsądnych” wartości domyślnych, z których dostarczania szczycą się zarówno Django, jak i Python. W przypadku wielu systemów nie musisz zajmować się bezpośrednio transakcjami, po prostu pozwól AUTOCOMMIT
wykonać swoją pracę. Ale jeśli tak, mam nadzieję, że ten post dostarczy ci informacji potrzebnych do zarządzania transakcjami w Django jak profesjonalista.