sql >> Database teknologi >  >> RDS >> Database

Transaktionsstyring med Django 1.6

Hvis du nogensinde har brugt meget tid på Django-databasetransaktionsstyring, ved du, hvor forvirrende det kan blive. Tidligere gav dokumentationen en del dybde, men forståelse kom kun gennem bygning og eksperimentering.

Der var et væld af dekoratører at arbejde med, såsom commit_on_success , commit_manually , commit_unless_managed , rollback_unless_managed , enter_transaction_management , leave_transaction_management , bare for at nævne nogle få. Heldigvis går det hele ud af døren med Django 1.6. Du behøver virkelig kun at vide om et par funktioner nu. Og dem kommer vi til på blot et sekund. Først vil vi behandle disse emner:

  • Hvad er transaktionsstyring?
  • Hvad er der galt med transaktionsstyring før Django 1.6?

Før du hopper ind i:

  • Hvad er rigtigt ved transaktionsstyring i Django 1.6?

Og så beskæftiger sig med et detaljeret eksempel:

  • Stripe-eksempel
  • Transaktioner
  • Den anbefalede måde
  • Brug af en dekoratør
  • Transaktion pr. HTTP-anmodning
  • SavePoints
  • Indlejrede transaktioner

Hvad er en transaktion?

Ifølge SQL-92 er "En SQL-transaktion (nogle gange blot kaldet en "transaktion") en sekvens af udførelser af SQL-sætninger, der er atomisk med hensyn til gendannelse". Med andre ord, alle SQL-sætninger udføres og forpligtes sammen. Ligeledes, når de rulles tilbage, bliver alle udsagn rullet sammen igen.

For eksempel:

# 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 er en enkelt arbejdsenhed i en database. Og den enkelte arbejdsenhed er afgrænset af en starttransaktion og derefter en commit eller en eksplicit rollback.



Hvad er der galt med transaktionsstyring før Django 1.6?

For at kunne besvare dette spørgsmål fuldt ud, skal vi behandle, hvordan transaktioner håndteres i databasen, klientbiblioteker og i Django.


Databaser

Hver erklæring i en database skal køre i en transaktion, selvom transaktionen kun indeholder én sætning.

De fleste databaser har en AUTOCOMMIT indstilling, som normalt er sat til True som standard. Denne AUTOCOMMIT omslutter hvert udsagn i en transaktion, der straks foretages, hvis udsagnet lykkes. Selvfølgelig kan du manuelt kalde noget som START_TRANSACTION som midlertidigt vil suspendere AUTOCOMMIT indtil du ringer til COMMIT_TRANSACTION eller ROLLBACK .

Men det vigtigste her er, at AUTOCOMMIT indstilling anvender en implicit commit efter hver erklæring .



Kundebiblioteker

Så er der Python klientbibliotekerne som sqlite3 og mysqldb, som tillader Python-programmer at interface med selve databaserne. Sådanne biblioteker følger et sæt standarder for, hvordan man får adgang til og forespørger databaserne. Denne standard, DB API 2.0, er beskrevet i PEP 249. Selvom det kan give lidt tør læsning, er en vigtig ting at tage imod, at PEP 249 angiver, at databasen AUTOCOMMIT skal være FRA som standard.

Dette er klart i konflikt med, hvad der sker i databasen:

  • SQL-sætninger skal altid køre i en transaktion, som databasen generelt åbner for dig via AUTOCOMMIT .
  • I henhold til PEP 249 bør dette dog ikke ske.
  • Kundebiblioteker skal afspejle, hvad der sker i databasen, men da de ikke har lov til at slå AUTOCOMMIT på som standard, pakker de blot dine SQL-sætninger ind i en transaktion, ligesom databasen.

Okay. Bliv hos mig lidt længere.



Django

Indtast Django. Django har også noget at sige om transaktionsstyring. I Django 1.5 og tidligere kørte Django dybest set med en åben transaktion og automatisk begået transaktionen, da du skrev data til databasen. Så hver gang du kaldte noget som model.save() eller model.update() , Django genererede de relevante SQL-sætninger og forpligtede transaktionen.

Også i Django 1.5 og tidligere blev det anbefalet, at du brugte TransactionMiddleware at binde transaktioner til HTTP-anmodninger. Hver anmodning fik en transaktion. Hvis svaret returnerede uden undtagelser, ville Django begå transaktionen, men hvis din visningsfunktion gav en fejl, ROLLBACK ville blive kaldt. Dette deaktiverede faktisk AUTOCOMMIT . Hvis du ville have standard, databaseniveau autocommit-stil transaktionsstyring, skulle du selv administrere transaktionerne - normalt ved at bruge en transaktionsdekorator på din visningsfunktion såsom @transaction.commit_manually , eller @transaction.commit_on_success .

Tage et åndedrag. Eller to.



Hvad betyder det?

Ja, der sker en masse der, og det viser sig, at de fleste udviklere bare vil have autocommits på standarddatabaseniveau - hvilket betyder, at transaktioner bliver bag kulisserne og gør deres ting, indtil du skal justere dem manuelt.




Hvad er rigtigt ved transaktionsstyring i Django 1.6?

Velkommen til Django 1.6. Gør dit bedste for at glemme alt, hvad vi lige har talt om, og husk blot, at i Django 1.6 bruger du databasen AUTOCOMMIT og administrere transaktioner manuelt, når det er nødvendigt. Grundlæggende har vi en meget enklere model, der grundlæggende gør, hvad databasen var designet til at gøre i første omgang.

Nok teori. Lad os kode.



Stripe Eksempel

Her har vi denne eksempelvisningsfunktion, der håndterer registrering af en bruger og opkald til Stripe for kreditkortbehandling.

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)
    )

Denne visning kalder først Customer.create som faktisk kalder Stripe til at håndtere kreditkortbehandlingen. Så opretter vi en ny bruger. Hvis vi fik et svar tilbage fra Stripe, opdaterer vi den nyoprettede kunde med stripe_id . Hvis vi ikke får en kunde tilbage (Stripe er nede), tilføjer vi en post til UnpaidUsers tabel med den nyoprettede kunders e-mail, så vi kan bede dem om at prøve deres kreditkortoplysninger igen senere.

Tanken er, at selvom Stripe er nede, kan brugeren stadig registrere sig og begynde at bruge vores side. Vi vil bare bede dem igen på et senere tidspunkt om kreditkortoplysningerne.

Jeg forstår, at dette kan være lidt af et konstrueret eksempel, og det er ikke den måde, jeg ville implementere en sådan funktionalitet, hvis jeg skulle, men formålet er at demonstrere transaktioner.

Fremad. Tænker på transaktioner og husker på, at Django 1.6 som standard giver os AUTOCOMMIT adfærd for vores database, lad os se på den databaserelaterede kode lidt længere.

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 se nogen problemer? Hvad sker der, hvis UnpaidUsers(email=cd['email']).save() linje fejler?

Du vil have en bruger, der er registreret i systemet, som systemet mener har bekræftet deres kreditkort, men i virkeligheden har de ikke bekræftet kortet.

Vi ønsker kun et af to resultater:

  1. Brugeren er oprettet (i databasen) og har et stripe_id .
  2. Brugeren er oprettet (i databasen) og har ikke et stripe_id OG en tilknyttet række i UnpaidUsers tabel med samme e-mailadresse genereres.

Hvilket betyder, at vi ønsker, at de to separate database-sætninger enten begge skal begå eller begge rollback. Et perfekt etui til den ydmyge transaktion.

Lad os først skrive nogle tests for at bekræfte, at tingene opfører sig, som vi vil have dem til.

@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)

Dekoratoren øverst i testen er en hån, der vil kaste en 'IntegrityError', når vi forsøger at gemme til UnpaidUsers tabel.

Dette er for at besvare spørgsmålet:"Hvad sker der, hvis UnpaidUsers(email=cd['email']).save() linjen fejler?" Den næste kodebit opretter bare en hånet session med de relevante oplysninger, vi har brug for til vores registreringsfunktion. Og så with mock.patch tvinger systemet til at tro, at Stripe er nede … endelig kommer vi til testen.

resp = register(self.request)

Ovenstående linje kalder blot vores registervisningsfunktion, der sender den hånede anmodning. Så tjekker vi bare for at sikre, at tabellerne ikke er opdateret:

#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 skulle mislykkes, hvis vi kører testen:

======================================================================
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æn. Det virker sjovt at sige, men det er præcis, hvad vi ønskede. Husk:vi praktiserer TDD her. Fejlmeddelelsen fortæller os, at brugeren faktisk bliver gemt i databasen - hvilket er præcis, hvad vi ikke ønsker, fordi de ikke har betalt!

Transaktioner til undsætning …



Transaktioner

Der er faktisk flere måder at oprette transaktioner på i Django 1.6.

Lad os gennemgå et par stykker.


Den anbefalede måde

Ifølge Django 1.6 dokumentation:

“Django leverer en enkelt API til at kontrollere databasetransaktioner. […] Atomicitet er den definerende egenskab ved databasetransaktioner. atomic giver os mulighed for at oprette en kodeblok, inden for hvilken atomiciteten på databasen er garanteret. Hvis kodeblokken er fuldført, overføres ændringerne til databasen. Hvis der er en undtagelse, rulles ændringerne tilbage.”

Atomic kan bruges som både dekoratør eller kontekstmanager. Så hvis vi bruger det som kontekststyring, vil koden i vores registerfunktion se sådan ud:

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')

Bemærk linjen with transaction.atomic() . Al kode inde i den blok vil blive udført i en transaktion. Så hvis vi kører vores test igen, burde de alle bestå! Husk, at en transaktion er en enkelt arbejdsenhed, så alt i kontekstadministratoren bliver rullet sammen igen, når UnpaidUsers opkald mislykkes.



Brug af en dekoratør

Vi kan også prøve at tilføje 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')

Hvis vi kører vores test igen, vil de mislykkes med den samme fejl, som vi havde før.

Hvorfor det? Hvorfor rullede transaktionen ikke tilbage korrekt? Årsagen er fordi transaction.atomic leder efter en slags undtagelse, og godt, vi fangede den fejl (dvs. IntegrityError i vores forsøg undtagen blok), så transaction.atomic har aldrig set det og dermed standarden AUTOCOMMIT funktionaliteten tog over.

Men selvfølgelig at fjerne forsøget undtagen vil medføre, at undtagelsen bare bliver kastet op i opkaldskæden og højst sandsynligt sprænges et andet sted. Så det kan vi heller ikke.

Så tricket er at sætte den atomiske kontekstadministrator inde i forsøget, undtagen blok, hvilket er, hvad vi gjorde i vores første løsning. Ser på den korrekte kode 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 udløser IntegrityError transaction.atomic() context manager vil fange det og udføre tilbagerulningen. På det tidspunkt, hvor vores kode udføres i undtagelsesbehandleren, (dvs. form.addError line) vil tilbagerulningen blive udført, og vi kunne trygt foretage databasekald, hvis det er nødvendigt. Bemærk også eventuelle databasekald før eller efter transaction.atomic() context manager vil være upåvirket uanset det endelige resultat af context_manager.



Transaktion pr. HTTP-anmodning

Django 1.6 (som 1.5) giver dig også mulighed for at arbejde i en "Transaktion pr. anmodning"-tilstand. I denne tilstand vil Django automatisk pakke din visningsfunktion ind i en transaktion. Hvis funktionen kaster en undtagelse vil Django rulle transaktionen tilbage, ellers vil den begå transaktionen.

For at få det opsat skal du indstille ATOMIC_REQUEST til True i databasekonfigurationen for hver database, som du vil have denne adfærd. Så i vores "settings.py" laver vi ændringen sådan her:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(SITE_ROOT, 'test.db'),
        'ATOMIC_REQUEST': True,
    }
}

I praksis opfører dette sig bare præcis, som hvis du sætter dekoratøren på vores udsigtsfunktion. Så det tjener ikke vores formål her.

Det er dog værd at bemærke, at med begge ATOMIC_REQUESTS og @transaction.atomic dekorator er det muligt stadig at fange / håndtere disse fejl, efter at de er smidt ud af udsigten. For at fange disse fejl bliver du nødt til at implementere noget tilpasset middleware, eller du kan tilsidesætte urls.hadler500 eller ved at lave en 500.html skabelon.




SavePoints

Selvom transaktioner er atomare, kan de opdeles yderligere i sparepunkter. Tænk på sparepunkter som delvise transaktioner.

Så hvis du har en transaktion, der tager fire SQL-sætninger at fuldføre, kan du oprette et lagringspunkt efter den anden sætning. Når først det lagringspunkt er oprettet, kan du, selvom 3. eller 4. sætning mislykkes, foretage en delvis tilbagerulning, slippe af med 3. og 4. sætning, men beholde de to første.

Så det er dybest set som at opdele en transaktion i mindre letvægtstransaktioner, der giver dig mulighed for at foretage delvise rollbacks eller commits.

Men husk, hvis hovedtransaktionen skal rulles tilbage (måske på grund af en IntegrityError der blev rejst og ikke fanget, så vil alle savepoint også blive rullet tilbage).

Lad os se på et eksempel på, hvordan savepoints fungerer.

@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)

Her er hele funktionen i en transaktion. Efter at have oprettet en ny bruger opretter vi et savepoint og får en reference til savepunktet. De næste tre udsagn-

user.name = 'starting down the rabbit hole'
user.stripe_id = 4
user.save()

-er ikke en del af det eksisterende savepoint, så de har chancen for at blive en del af den næste savepoint_rollback , eller savepoint_commit . I tilfælde af en savepoint_rollback , linjen user = User.create('jj','inception','jj','1234') vil stadig være forpligtet til databasen, selvom resten af ​​opdateringerne ikke gør det.

Sagt på en anden måde beskriver disse følgende to test, hvordan lagringspunkterne fungerer:

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')

Også efter at vi har forpligtet eller rullet tilbage et savepoint, kan vi fortsætte med at udføre arbejde i den samme transaktion. Og det arbejde vil være upåvirket af resultatet af det forrige savepoint.

For eksempel hvis vi opdaterer vores save_points fungere 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')

Uanset om savepoint_commit eller savepoint_rollback blev kaldt 'limbo'-brugeren vil stadig blive oprettet. Medmindre noget andet får hele transaktionen til at blive rullet tilbage.



Indlejrede transaktioner

Ud over manuelt at angive sparepunkter, med savepoint() , savepoint_commit , og savepoint_rollback , vil oprettelse af en indlejret transaktion automatisk oprette et lagringspunkt for os og rulle det tilbage, hvis vi får en fejl.

Hvis vi udvider vores eksempel lidt længere, 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

Her kan vi se, at efter at vi har behandlet vores savepoints, bruger vi transaction.atomic kontekstmanager for at omslutte vores skabelse af 'limbo'-brugeren. Når kontekstadministratoren kaldes, er det i realiteten at skabe et lagringspunkt (fordi vi allerede er i en transaktion), og det lagringspunkt vil blive forpligtet eller rullet tilbage, når kontekstadministratoren forlades.

Således beskriver de følgende to test deres adfærd:

 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 virkeligheden kan du bruge enten atomic eller savepoint at oprette sparepunkter i en transaktion. Med atomic du behøver ikke at bekymre dig eksplicit om commit / rollback, hvor som med savepoint du har fuld kontrol over, hvornår det sker.



Konklusion

Hvis du tidligere har haft erfaring med tidligere versioner af Django-transaktioner, kan du se, hvor meget enklere transaktionsmodellen er. Har også AUTOCOMMIT on som standard er et godt eksempel på "fornemme" standarder, som Django og Python begge er stolte af at levere. For mange systemer behøver du ikke at håndtere transaktioner direkte, lad bare AUTOCOMMIT gøre sit arbejde. Men hvis du gør det, vil dette indlæg forhåbentlig have givet dig de oplysninger, du har brug for til at administrere transaktioner i Django som en professionel.



  1. Sådan automatiseres udrulning af PostgreSQL-database

  2. Sådan fjerner du dublerede rækker i SQL

  3. Pil notation

  4. Salesforce TLS 1.0 udfasning