sql >> Databáze >  >> RDS >> Database

Správa transakcí s Django 1.6

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ů:

  1. Uživatel je vytvořen (v databázi) a má stripe_id .
  2. Uživatel je vytvořen (v databázi) a nemá stripe_id A přidružený řádek v UnpaidUsers 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.



  1. LEFT() vs SUBSTRING() v SQL Server:Jaký je rozdíl?

  2. Pokračování transakce po chybě porušení primárního klíče

  3. Jak získat záznamy za posledních 15 dní v MySQL

  4. Rozdíl mezi sys.views, sys.system_views a sys.all_views na serveru SQL Server