Database
 sql >> база данни >  >> RDS >> Database

Управление на транзакции с Django 1.6

Ако някога сте отделяли много време на управлението на транзакциите на база данни на Django, знаете колко объркващо може да стане. В миналото документацията предоставяше доста дълбочина, но разбирането идваше само чрез изграждане и експериментиране.

Имаше множество декоратори за работа, като commit_on_success , commit_manually , commit_unless_managed , rollback_unless_managed , enter_transaction_management , leave_transaction_management , само да назовем няколко. За щастие, с Django 1.6 всичко излиза навън. Сега наистина трябва да знаете само за няколко функции. И ще стигнем до тях само за секунда. Първо, ще разгледаме тези теми:

  • Какво е управление на транзакциите?
  • Какво не е наред с управлението на транзакциите преди Django 1.6?

Преди да преминете към:

  • Какво е правилно в управлението на транзакциите в Django 1.6?

И след това се занимаваме с подробен пример:

  • Пример на ивица
  • Транзакции
  • Препоръчания начин
  • Използване на декоратор
  • Транзакция по HTTP заявка
  • SavePoints
  • Вложени транзакции

Какво е транзакция?

Според SQL-92 „SQL транзакция (понякога просто наричана „транзакция“) е последователност от изпълнения на SQL оператори, която е атомарна по отношение на възстановяването“. С други думи, всички SQL оператори се изпълняват и записват заедно. По същия начин, когато се връщат назад, всички изрази се връщат заедно.

Например:

# START
note = Note(title="my first note", text="Yay!")
note = Note(title="my second note", text="Whee!")
address1.save()
address2.save()
# COMMIT

Така че транзакцията е единична единица работа в база данни. И тази единична работна единица е разграничена от начална транзакция и след това комит или изрично връщане назад.



Какво не е наред с управлението на транзакциите преди Django 1.6?

За да отговорим напълно на този въпрос, трябва да разгледаме как се обработват транзакциите в базата данни, клиентските библиотеки и в Django.


Бази данни

Всеки израз в база данни трябва да се изпълнява в транзакция, дори ако транзакцията включва само един израз.

Повечето бази данни имат AUTOCOMMIT настройка, която обикновено е зададена на True по подразбиране. Това AUTOCOMMIT обвива всяко изявление в транзакция, която се извършва незабавно, ако изявлението успее. Разбира се, можете ръчно да извикате нещо като START_TRANSACTION което временно ще спре AUTOCOMMIT докато не извикате COMMIT_TRANSACTION или ROLLBACK .

Въпреки това, изводът тук е, че AUTOCOMMIT настройката прилага имплицитен комит след всяко изявление .



Клиентски библиотеки

След това има клиентските библиотеки на Python като sqlite3 и mysqldb, които позволяват на програмите на Python да взаимодействат със самите бази данни. Такива библиотеки следват набор от стандарти за достъп и запитване до базите данни. Този стандарт, DB API 2.0, е описан в PEP 249. Въпреки че може да доведе до малко сухо четене, важен извод е, че PEP 249 посочва, че базата данни AUTOCOMMIT трябва да е ИЗКЛЮЧЕНО по подразбиране.

Това явно противоречи на случващото се в базата данни:

  • SQL изразите винаги трябва да се изпълняват в транзакция, която базата данни обикновено отваря за вас чрез AUTOCOMMIT .
  • Съгласно PEP 249 обаче това не трябва да се случва.
  • Клиентските библиотеки трябва да отразяват това, което се случва в базата данни, но тъй като не им е позволено да включат AUTOCOMMIT по подразбиране, те просто обгръщат вашите SQL изрази в транзакция, точно като базата данни.

Добре. Остани с мен още малко.



Django

Влезте в Django. Джанго също има какво да каже за управлението на транзакциите. В Django 1.5 и по-стари, Django основно се изпълняваше с отворена транзакция и автоматично извършваше тази транзакция, когато сте писали данни в базата данни. Така че всеки път, когато извикате нещо като model.save() или model.update() , Django генерира съответните SQL оператори и извършва транзакцията.

Също така в Django 1.5 и по-стари се препоръчва да използвате TransactionMiddleware за свързване на транзакции към HTTP заявки. На всяка заявка беше дадена транзакция. Ако отговорът се върне без изключения, Django ще извърши транзакцията, но ако функцията ви за преглед пусне грешка, ROLLBACK би се наричал. Това на практика изключи AUTOCOMMIT . Ако искате стандартно управление на транзакциите в стил на автоматично записване на ниво база данни, трябваше да управлявате транзакциите сами - обикновено с помощта на декоратор на транзакции във вашата функция за изглед, като @transaction.commit_manually или @transaction.commit_on_success .

Поеми си дъх. Или две.



Какво означава това?

Да, там се случват много неща и се оказва, че повечето разработчици просто искат стандартните автоматични ангажименти на ниво база данни – което означава, че транзакциите остават зад кулисите, вършат своето, докато не се наложи да ги коригирате ръчно.




Какво е правилно в управлението на транзакциите в Django 1.6?

Сега, добре дошли в Django 1.6. Направете всичко възможно да забравите всичко, за което току-що говорихме, и просто запомнете, че в Django 1.6 използвате база данни AUTOCOMMIT и управлявайте транзакциите ръчно, когато е необходимо. По същество имаме много по-опростен модел, който по същество прави това, за което базата данни е била проектирана на първо място.

Стига теория. Да кодираме.



Пример на ивица

Тук имаме тази примерна функция за преглед, която обработва регистрацията на потребител и извикването на Stripe за обработка на кредитна карта.

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

Този изглед първо извиква Customer.create което всъщност се обажда на Stripe, за да се справи с обработката на кредитни карти. След това създаваме нов потребител. Ако получим отговор от Stripe, актуализираме новосъздадения клиент с stripe_id . Ако не получим клиент обратно (Stripe не работи), ще добавим запис към UnpaidUsers таблица с новосъздадения имейл на клиенти, за да можем да ги помолим да опитат отново данните за кредитната си карта по-късно.

Идеята е, че дори ако Stripe не работи, потребителят все още може да се регистрира и да започне да използва нашия сайт. Просто ще ги попитаме отново на по-късна дата за информацията за кредитната карта.

Разбирам, че това може да е малко измислен пример и не е начинът, по който бих внедрил такава функционалност, ако трябва, но целта е да демонстрирам транзакции.

Напред. Мислейки за транзакции и имайки предвид, че по подразбиране Django 1.6 ни дава AUTOCOMMIT поведение за нашата база данни, нека разгледаме кода, свързан с базата данни, малко по-дълго.

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:
    # ...

Можете ли да забележите някакви проблеми? Какво ще стане, ако UnpaidUsers(email=cd['email']).save() линията не работи?

Ще имате потребител, регистриран в системата, за който системата смята, че е потвърдил кредитната си карта, но в действителност той не е потвърдил картата.

Искаме само един от двата резултата:

  1. Потребителят е създаден (в базата данни) и има stripe_id .
  2. Потребителят е създаден (в базата данни) и няма stripe_id И свързан ред в UnpaidUsers генерира се таблица със същия имейл адрес.

Което означава, че искаме двата отделни израза на базата данни или да завършат, или и двете да се върнат обратно. Перфектен калъф за скромната транзакция.

Първо, нека напишем някои тестове, за да проверим, че нещата се държат така, както искаме.

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

Декораторът в горната част на теста е макет, който ще изведе „IntegrityError“, когато се опитаме да запишем в UnpaidUsers таблица.

Това е, за да се отговори на въпроса „Какво се случва, ако UnpaidUsers(email=cd['email']).save() линията не работи?" Следващият бит код просто създава подигравателна сесия с подходящата информация, от която се нуждаем за нашата функция за регистрация. И след това with mock.patch принуждава системата да вярва, че Stripe не работи... накрая стигаме до теста.

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)

Така че трябва да се провали, ако изпълним теста:

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

----------------------------------------------------------------------

Хубаво. Изглежда смешно да се каже, но точно това искахме. Запомнете:ние практикуваме TDD тук. Съобщението за грешка ни казва, че Потребителят наистина се съхранява в базата данни – точно това, което не искаме, защото не е платил!

Транзакциите на помощ...



Транзакции

Всъщност има няколко начина за създаване на транзакции в Django 1.6.

Нека преминем през няколко.


Препоръчителният начин

Според документацията на Django 1.6:

„Django предоставя единен API за контрол на транзакциите в базата данни. […] Атомарността е определящото свойство на транзакциите в базата данни. atomic ни позволява да създадем блок от код, в който атомарността в базата данни е гарантирана. Ако блокът от код е завършен успешно, промените се записват в базата данни. Ако има изключение, промените се връщат назад.”

Atomic може да се използва както като декоратор, така и като context_manager. Така че, ако го използваме като контекстен мениджър, кодът в нашата функция за регистър ще изглежда така:

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

Обърнете внимание на реда with transaction.atomic() . Целият код в този блок ще бъде изпълнен в транзакция. Така че, ако пуснем отново нашите тестове, всички те трябва да преминат! Не забравяйте, че транзакцията е единична работна единица, така че всичко в контекстния мениджър се връща заедно, когато UnpaidUsers обаждането е неуспешно.



Използване на декоратор

Можем също да опитаме да добавим atomic като декоратор.

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

Ако изпълним отново нашите тестове, те ще се провалят със същата грешка, която имахме преди.

Защо така? Защо транзакцията не се върна правилно? Причината е, че transaction.atomic търси някакво изключение и добре, ние хванахме тази грешка (т.е. IntegrityError в нашия опит освен блок), така че transaction.atomic никога не съм го виждал и следователно стандартният AUTOCOMMIT функционалността пое.

Но, разбира се, премахването на опита с изключение ще доведе до това изключението просто да бъде изхвърлено нагоре по веригата за повиквания и най-вероятно да се взриви някъде другаде. Така че и ние не можем да направим това.

Така че трикът е да поставите атомарния контекстен мениджър в блока try освен блок, което направихме в първото си решение. Преглеждайки отново правилния код:

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

Когато UnpaidUsers задейства IntegrityError transaction.atomic() контекстният мениджър ще го хване и ще извърши връщане назад. Докато кодът ни се изпълни в манипулатора на изключения, (т.е. form.addError линия) връщането ще бъде извършено и можем безопасно да извършваме извиквания на база данни, ако е необходимо. Също така имайте предвид всички извиквания на база данни преди или след transaction.atomic() контекстния мениджър няма да бъде засегнат, независимо от крайния резултат на context_manager.



Транзакция по HTTP заявка

Django 1.6 (като 1.5) също ви позволява да работите в режим „Транзакция по заявка“. В този режим Django автоматично ще обвие вашата функция за преглед в транзакция. Ако функцията хвърли изключение, Django ще върне обратно транзакцията, в противен случай ще извърши транзакцията.

За да го настроите, трябва да зададете ATOMIC_REQUEST на True в конфигурацията на базата данни за всяка база данни, която искате да има това поведение. Така че в нашия “settings.py” правим промяната по следния начин:

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

На практика това просто се държи точно така, сякаш поставите декоратора на нашата функция за преглед. Така че тук не служи за целите ни.

Все пак си струва да се отбележи, че и с двата ATOMIC_REQUESTS и @transaction.atomic декоратор е възможно все още да улавя/обработва тези грешки, след като бъдат изхвърлени от изгледа. За да хванете тези грешки, ще трябва да внедрите някакъв персонализиран междинен софтуер или можете да замените urls.hadler500 или като направите шаблон 500.html.




SavePoints

Въпреки че транзакциите са атомарни, те могат да бъдат допълнително разбити на точки за запазване. Мислете за точките за запис като частични транзакции.

Така че, ако имате транзакция, която изисква четири SQL оператора за завършване, можете да създадете точка за запис след втория израз. След като тази точка за запазване бъде създадена, дори ако 3-то или 4-то изявление не успее, можете да направите частично връщане назад, като се отървете от 3-то и 4-то изявление, но запазвайки първите две.

Така че по същество е като разделянето на транзакция на по-малки леки транзакции, което ви позволява да правите частично връщане назад или ангажименти.

Но имайте предвид дали основната транзакция трябва да бъде върната назад (може би поради IntegrityError който е бил повдигнат и не е хванат, тогава всички точки за запис също ще бъдат върнати обратно).

Нека разгледаме пример за това как работят точките за запис.

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

Тук цялата функция е в транзакция. След като създадем нов потребител, създаваме точка за запис и получаваме препратка към точката за запис. Следващите три твърдения-

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

-не са част от съществуващата точка за запис, така че имат шанса да бъдат част от следващия savepoint_rollback , или savepoint_commit . В случай на savepoint_rollback , редът user = User.create('jj','inception','jj','1234') ще продължат да бъдат ангажирани с базата данни, въпреки че останалите актуализации не.

Казано по друг начин, следните два теста описват как работят точките за запис:

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

Също така, след като извършим или връщаме назад точка на запис, можем да продължим да работим в същата транзакция. И тази работа няма да бъде повлияна от резултата от предишната точка на запис.

Например, ако актуализираме нашите save_points функция като такава:

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

Независимо дали savepoint_commit или savepoint_rollback е наречен „limbo“ потребителят все пак ще бъде създаден успешно. Освен ако нещо друго не доведе до връщане на цялата транзакция.



Вложени транзакции

В допълнение към ръчното указване на точки за запис, с savepoint() , savepoint_commit и savepoint_rollback , създаването на вложена транзакция автоматично ще създаде точка за запис за нас и ще я върне обратно, ако получим грешка.

Разширявайки нашия пример още малко, получаваме:

@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

Тук можем да видим, че след като се справим с нашите точки за запис, ние използваме transaction.atomic контекстен мениджър, за да обвием нашето създаване на потребителя „limbo“. Когато този мениджър на контекста бъде извикан, той на практика създава точка за запис (тъй като вече сме в транзакция) и тази точка на запис ще бъде ангажирана или връщана назад при излизане от диспечера на контекста.

По този начин следните два теста описват тяхното поведение:

 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)

Така че в действителност можете да използвате или atomic или savepoint за създаване на точки за запис в транзакция. С atomic не е нужно да се притеснявате изрично за комит / връщане назад, където както с savepoint имате пълен контрол над това кога това се случва.



Заключение

Ако сте имали предишен опит с по-ранни версии на транзакциите на Django, можете да видите колко по-опростен е моделът на транзакциите. Също така има AUTOCOMMIT on по подразбиране е чудесен пример за „разумни“ настройки по подразбиране, с които и Django, и Python се гордеят. За много системи няма да е необходимо да се занимавате директно с транзакции, просто оставете AUTOCOMMIT върши работата си. Но ако го направите, надяваме се, че тази публикация ще ви даде информацията, от която се нуждаете, за да управлявате транзакциите в Django като професионалист.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Как лесно да разположите TimescaleDB

  2. Избягване на сортиране с конкатенация на присъединяване при сливане

  3. Разликата между първичен ключ и уникален ключ

  4. SQL не е равен на () оператор за начинаещи

  5. Ограничаване на гъвкавостта на данните в NoSQL база данни