?如果你花費(fèi)了很多的時間去進(jìn)行Django數(shù)據(jù)庫事務(wù)處理的話,你將會了解到這是讓人暈頭轉(zhuǎn)向的。
在過去,只是提供了簡單的基礎(chǔ)文檔,要想清楚知道它是怎么使用的,還必須要通過創(chuàng)建和執(zhí)行Django的事務(wù)處理。
這里有眾多的Django事務(wù)處理的名詞,例如:commit_on_success , commit_manually , commit_unless_maneged,rollback_unless_managed,enter_transaction_management,leace_transaction_management,這些只是其中的一部分而已。
最讓人感到幸運(yùn)的是,Django 1.6版本發(fā)布后。現(xiàn)在你可以緊緊使用幾個函數(shù)就可以實(shí)現(xiàn)事務(wù)處理了,在我們進(jìn)入學(xué)習(xí)這些函數(shù)的幾秒之前。首先,我們要明確下面這些問題:
- ??? 什么是事務(wù)處理呢?
- ??? Django 1.6中的出現(xiàn)了那些新的事物處理優(yōu)先權(quán)?
在進(jìn)入“要怎么才是Django 1.6版本中正確的事務(wù)處理”之前,請先了解一下下面的詳細(xì)案例:
??? 帶狀案例
??? 事務(wù)
- ??????????? 推薦的方式
- ??????????? 使用分隔符
- ??????????? 每一個HTTP請求事務(wù)
??? 保存點(diǎn)
??? 嵌套的事務(wù)
事務(wù)是什么?
根據(jù)SQL-92所說,"一個SQL事務(wù)(有時簡稱為事務(wù))是一個可以原子性恢復(fù)的SQL語句執(zhí)行序列"。換句話說,所有的SQL語句一起執(zhí)行和提交。同樣的,當(dāng)回滾時,所有語句也一并回滾。
例如:
?
# START note = Note(title="my first note", text="Yay!") note = Note(title="my second note", text="Whee!") address1.save() address2.save() # COMMIT
所以一個事務(wù)是 數(shù)據(jù)庫中單個的工作單元。它是由一個start transaction開始和一個commit或者顯式的rollback結(jié)束。
Django 1.6之前的事務(wù)管理有什么問題?
為了完整地回答這個問題,我們必須闡述一下事務(wù)在數(shù)據(jù)庫、客戶端以及Django中是如何處理的。
數(shù)據(jù)庫
數(shù)據(jù)庫中的每一條語句都運(yùn)行在一個事務(wù)中,這個事務(wù)甚至可以只包含一條語句。
幾乎所有的數(shù)據(jù)庫都有一個AUTOCOMMIT設(shè)置,通常它被默認(rèn)設(shè)置為True。AUTOCOMMIT將所有語句包裝到一個事務(wù)里,只要語句成功執(zhí)行,這個事務(wù)就立即被提交。當(dāng)然你也可以手動調(diào)用START_TRANSACTION,它會暫時將AUTOCOMMIT掛起,直到你調(diào)用COMMIT_TRANSACTION或者ROLLBACK。
然后,這種方式將會使AUTOCOMMIT設(shè)置的作用于每條語句后的隱式提交失效。
然而,有諸如像sqlite3和mysqldb的python客戶端庫,它允許python程序與數(shù)據(jù)庫本身相連接。這些庫遵循一套如何訪問與查詢數(shù)據(jù)庫的標(biāo)準(zhǔn)。該DB API 2.0標(biāo)準(zhǔn),被描述在PEP 249之中。雖然它可能讓人閱讀稍干一些。一個重要帶走的是,在PEP 249狀態(tài)之中,默認(rèn)數(shù)據(jù)庫應(yīng)該關(guān)閉自動提交功能。
這明顯與數(shù)據(jù)庫內(nèi)發(fā)生了什么矛盾沖突:
- ??? 數(shù)據(jù)庫語句總是要在一個事務(wù)中運(yùn)行。此數(shù)據(jù)庫一般會為你打開自動提交功能。
- ??? 不過,根據(jù)PEP 249,這不應(yīng)該發(fā)生。
- ??? 客戶端庫必須反映在數(shù)據(jù)庫之中發(fā)生了什么?但由于他們不允許默認(rèn)打開自動提交功能。他們只是簡單地在一個事務(wù)中包裹sql語句。就像數(shù)據(jù)庫。
好啦,在我身邊呆久一些吧。
?
Django
進(jìn)入Django,Django也有關(guān)于事務(wù)處理的話要說。在Django 1.5和更早的版本。當(dāng)你寫數(shù)據(jù)到數(shù)據(jù)庫時,Django基本上是運(yùn)行一個開放的事務(wù)和自動提交該事務(wù)功能。所以每次你所稱謂的像諸如model.save() 或者model.update()的東西,Django生成相應(yīng)的sql語句,并提交該事務(wù)。
也有在Django 1.5和更早的版本,它是建議你使用TransactionMiddleware綁定http請求事務(wù)。每個請求提供了一個事務(wù)。如果返回的響應(yīng)沒有異常,Django會提交此事務(wù)。但如果你的視圖功能拋出一個錯誤,回滾將被調(diào)用。這實(shí)際上說明,它關(guān)閉了自動提交功能。如果你想要標(biāo)準(zhǔn)化,數(shù)據(jù)庫級別自動提交風(fēng)格式的事務(wù)管理,你必須管理你自己的交易-通常是通過使用事務(wù)裝飾你的視圖功能,例如@transaction.commit_manually,或者@transaction.commit_on_success.
吸一口氣,或者兩口。
這意味著什么?
是啊,在那兒有許多事情要做,而事實(shí)證明,大多數(shù)開發(fā)者正需要這個標(biāo)準(zhǔn)數(shù)據(jù)庫級的自動提交功能-有意義的事務(wù)往往是留在幕后處理的。做你自己的事,直到你需要手動調(diào)整他們。
在Django 1.6版本之中,什么是正確關(guān)于事務(wù)管理呢?
現(xiàn)在,歡迎來到Django 1.6.盡力忘掉一切吧,我們只是談?wù)摱眩皇怯浀迷贒jango 1.6中,你可以使用數(shù)據(jù)庫,需要時可以手動自動提交和管理事務(wù)。從本質(zhì)上來說,我們有一個更簡單的模型,基本上是要把設(shè)計(jì)什么樣的數(shù)據(jù)庫擺在首位。
好啦!大功告成,讓我們寫代碼吧?
?
Stripe案例
下面,我們使用處理一個用戶注冊的例子,調(diào)用了Stripe來處理信用卡進(jìn)程。
?
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) )
例子首先調(diào)用了Customer.create,實(shí)際上就是調(diào)用Stripe來處理信用卡進(jìn)程,然后我們創(chuàng)建一個新用戶。如果我們得到來自Stripe的響應(yīng),我們就用stripe_id更新新創(chuàng)建的用戶。如果我們沒有得到響應(yīng)(Stripe已關(guān)閉),我們將用新創(chuàng)建用戶的email向UnpaidUsers表增加一個新條目,這樣我們可以讓他們稍后重試他們信用卡信息。
思路是這樣的:如果Stripe沒有響應(yīng),用戶依然可以注冊,然后開始使用我們的網(wǎng)站。我們將在稍后的時候讓用戶提供信用卡的信息。
“我明白這是一個特殊的例子,并且這也不是我想完成的功能的方式,但是它的目的是展示交易”
考慮交易,牢記住在Django1.6中提供了對于數(shù)據(jù)庫的“AUTOCOMMIT”功能。接下來看一下數(shù)據(jù)庫相關(guān)的代碼:
?
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:
你能發(fā)現(xiàn)問題了嗎?如果“UnpaidUsers(email=cd['email']).save()” 運(yùn)行失敗,會發(fā)生什么?
有一個用戶,注冊了系統(tǒng);然后系統(tǒng)認(rèn)為已經(jīng)核對過信用卡了。但是事實(shí)上,系統(tǒng)并沒有核對過。
我們僅僅想得到其中一種結(jié)果:
1.在數(shù)據(jù)庫中創(chuàng)建了用戶,并有了stripe_id
2.在數(shù)據(jù)庫中創(chuàng)建了用戶,但是沒有stripe_id。同時在相關(guān)的“UnpaidUsers”行,存有相同的郵件地址
這就意味著,我們想要的是分開的數(shù)據(jù)庫語句頭完成任務(wù)或者回滾。這個例子很好的說明了這個交易。
首先,我們寫一些測試用例來驗(yàn)證事情是否按照我們想象的方式運(yùn)行:?
?
@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' : 'python@rocks.com', '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="python@rocks.com") self.assertEquals(len(users), 0) #check the associated table also didn't get updated unpaid = UnpaidUsers.objects.filter(email="python@rocks.com") self.assertEquals(len(unpaid), 0)
當(dāng)我們嘗試去保存“UnpaidUsers”,測試上方的解釋器就會跑出異常'IntegrityError' 。
接下來是解釋這個問題的答案,“當(dāng)“UnpaidUsers(email=cd['email']).save()”運(yùn)行的時候到底發(fā)生了什么?” 下面一段代碼創(chuàng)建了一段對話,我們需要在注冊函數(shù)中給出一些合適的信息。然后“with mock.patch” 會強(qiáng)制系統(tǒng)去認(rèn)為Stripe沒響應(yīng),最終就跳到我們的測試用例中。
resp = register(self.request)
上面這段話僅僅是調(diào)用我們的注冊視圖去傳遞請求。然后我們僅僅需要去核對表是否有更新:
?
#assert there is no record in the database without stripe_id. users = User.objects.filter(email="python@rocks.com") self.assertEquals(len(users), 0) #check the associated table also didn't get updated unpaid = UnpaidUsers.objects.filter(email="python@rocks.com") self.assertEquals(len(unpaid), 0)
所以如果我們運(yùn)行了測試用例,那么它就該運(yùn)行失敗:
?
====================================================================== 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 ----------------------------------------------------------------------
贊。這就是我們最終想要的結(jié)果。
記住:我們這里已經(jīng)練習(xí)了“測試驅(qū)動開發(fā)”的能力。錯誤信息提示我們:用戶信息已經(jīng)被保存到數(shù)據(jù)庫中,但是這個并不是我們想要的,因?yàn)槲覀儾]有付費(fèi)!
事務(wù)交易用于挽救這樣問題 ...
事務(wù)
對于Django1.6,有很多種方式來創(chuàng)建事務(wù)。
這里簡單介紹幾種。
推薦的方法
依據(jù)Django1.6的文檔,“Django提供了一種簡單的API去控制數(shù)據(jù)庫的事務(wù)交易...原子操作用來定義數(shù)據(jù)庫事務(wù)的屬性。原子操作允許我們在數(shù)據(jù)庫保證的前提下,創(chuàng)建一堆代碼。如果這些代碼被成功的執(zhí)行,所對應(yīng)的改變也會提交到數(shù)據(jù)庫中。如果有異常發(fā)生,那么操作就會回滾。”
原子操作可以被用于解釋操作或者是內(nèi)容管理。所以如果我們用作為內(nèi)容管理的時候,我們的注冊函數(shù)的代碼就會如下:
?
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()”這一行。這塊代碼將會在事務(wù)內(nèi)部執(zhí)行。所以如果我們重新運(yùn)行了我們的測試,他們都將會通過。
記住:事務(wù)是一個工作單元,所以當(dāng)“UnpaidUsers”調(diào)用失敗的時候,內(nèi)容管理的所有操作都會被一起回滾。
使用裝飾器
除了上面的做法,我們能使用Python的裝飾器特性來使用事務(wù)。
?
@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')
如果我們重新跑一次測試,那還是會一樣失敗。
為啥呢?為啥事務(wù)沒有正確回滾呢?原因在與transaction.atomic會嘗試捕獲某種異常,而我們代碼里人肉捕抓了(例如 try-except 代碼塊里的IntegrityError 異常),所以 transaction.atomic 永遠(yuǎn)看不到這個異常,所以標(biāo)準(zhǔn)的AUTOCOMMIT 流程就此無效掉。
但是,刪掉try-catch語句會導(dǎo)致異常沒有捕獲,然后代碼流程十有八九會就此亂掉。所以啊,也就是說不能去掉try-catch。
?
所以,技巧是將原子上下文管理器放入我們在第一個解決方案中的 try-catch 代碼段里。
再看一下正確的代碼:
?
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')
當(dāng) UnpaidUsers 觸發(fā) IntegrityError 時,上下文管理器 transaction.atomic() 會捕獲到它,并執(zhí)行回滾操作。此時我們的代碼在異常處理中執(zhí)行(即行 theform.addErrorline),將會完成回滾操作,并且,如果必要的話,也可以安全的進(jìn)行數(shù)據(jù)庫調(diào)用。也要注意:任何在上下文管理器 thetransaction.atomic() 前后的數(shù)據(jù)庫調(diào)用都不會受到它的執(zhí)行結(jié)果的影響。
針對每次HTTP請求的事務(wù)交易
Django1.5和1.6版本都允許用戶操作請求事務(wù)模式。在這種模式下,Django會自動在事務(wù)中,處理你的視圖函數(shù)。如果視圖函數(shù)拋出異常,Django會自動回滾事務(wù);否則Django會提交事務(wù)。
為了實(shí)現(xiàn)這個功能,你需要在你想要有此功能的數(shù)據(jù)庫的配置中,設(shè)置“ATOMIC_REQUEST”為真。所以在我們的“settings.py”需要有如下設(shè)置:
?
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(SITE_ROOT, 'test.db'), 'ATOMIC_REQUEST': True, } }
如果我們把解釋器放到視圖函數(shù)中,以上設(shè)置就會生效。所以這并沒有符合我們的想法。
但是,這里依然值得注意的是解釋器:“ATOMIC_REQUESTS”和“@transaction.atomic”仍然會有可能在有異常拋出的時候,處理這些錯誤。為了去捕捉這些錯誤,你需要去完成一些常規(guī)的中間件,或者需要去覆蓋“urls.hadler500”,或者是創(chuàng)建新的“500.html”模板。
保存點(diǎn)
盡管事務(wù)是有原子性的,但還是能夠打散為多個“保存點(diǎn)”――你可看作是“部分事務(wù)”。
例如,你有個事務(wù)包含了4條SQL語句,你可以在第二個SQL之后創(chuàng)建一個保存點(diǎn)。一旦保存點(diǎn)創(chuàng)建成功,就算第三條或第四條SQL執(zhí)行失敗,你仍舊能夠做一個部分回滾,忽視后面兩條SQL,僅保留前面兩條。
基本上,這就像是提供了一個切割的能力:一個普通的事務(wù)能夠被切分為多個更“輕量”的事務(wù),然后能進(jìn)行部分回滾或部分提交。
??? 但一定要注意,小心整個事務(wù)被無端回滾掉(例如由于拋出了IntegrityError異常但卻沒有捕抓,那所有的保存點(diǎn)都會因此被回滾掉的)
來看個示例代碼,了解怎么玩轉(zhuǎn)保存點(diǎn)。
?
@transaction.atomic() def save_points(self,save=True): user = User.create('jj','inception','jj','1234') sp1 = transaction.savepoint() user.name = 'zheli hui guadiao, T.T' user.stripe_id = 4 user.save() if save: transaction.savepoint_commit(sp1) else: transaction.savepoint_rollback(sp1)
示例中,整個函數(shù)都是屬于一個事務(wù)的。新User對象創(chuàng)建后,我們創(chuàng)建并得到了一個保存點(diǎn)。然后后續(xù)3行語句――
?
user.name = 'zheli hui guadiao, T.T' user.stripe_id = 4 user.save()
――不屬于剛才的保存點(diǎn),因此他們有可能是屬于下面的savepoint_rollback或savepoint_commit的一部分。假設(shè)是savepoint_rollback, 那代碼行user = User.create('jj','inception','jj','1234')仍舊會成功提交到數(shù)據(jù)庫中 ,而下面三行則不會成功。
?
采用另外一種方法,下面的兩種測試用例描述了保存點(diǎ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) #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')
同樣,在我們提交或者回滾保存點(diǎn)之后,我們?nèi)匀豢梢岳^續(xù)在同一個事務(wù)中工作。同時,這個運(yùn)行結(jié)果不受之前保存點(diǎn)輸出結(jié)果的影響。
例如,如果我們按照如下例子更新“save_points”函數(shù),
?
@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”被“l(fā)imbo”這個用戶調(diào)用了,這個事務(wù)仍然會被成功創(chuàng)建。如果沒有創(chuàng)建成功,整個事務(wù)將會被回滾。
嵌套事務(wù)
采用“savepoint()”,“savepoint_commit”和“savepoint_rollback”去手動指定保存點(diǎn),將會自動一個嵌套事務(wù),同時這個嵌套事務(wù)會自動為我們創(chuàng)建一個保存點(diǎn)。并且,如果我們遇到錯誤,這個事務(wù)將會回滾。
下面用一個擴(kuò)展的例子來說明:
?
@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
這里我們可以看到:在我們處理保存點(diǎn)之后,我們采用“thetransaction.atomic”的上下文管理區(qū)擦出我們創(chuàng)建的"limbo"這個用戶。當(dāng)上下文管理被調(diào)用的時候,它會創(chuàng)建一個保存點(diǎn)(因?yàn)槲覀円呀?jīng)在事務(wù)里面了),同時這個保存點(diǎn)將會依據(jù)已經(jīng)存在的上下文管理器去被執(zhí)行或者回滾。
這樣下面兩個測試用例就描述了這個行文:
?
?
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)
因此,在現(xiàn)實(shí)之中你可以使用原子或者在事務(wù)之中創(chuàng)建保存點(diǎn)的保存點(diǎn)。使用原子,你不必要很仔細(xì)地?fù)?dān)心提交和會滾,當(dāng)這種情況發(fā)生時,你可以完全控制其中的保存點(diǎn)。
結(jié)論
如果你有任何以往使用Django更早版本事務(wù)處理的經(jīng)驗(yàn),你可以看到很多更簡單地事務(wù)處理模型。如下,在默認(rèn)情況下,也有自動提交功能,它是一個很好的例子,即Django與python兩者都引以為豪所提供的“理智的”默認(rèn)值。對于如此多的系統(tǒng),你將不需要直接地來處理事務(wù)。只是讓“自動提交功能”來完成其工作,但如果你這樣做,我將希望這個帖子能提供你所需要像專家一樣在Django之中管理的事務(wù)處理。
?
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長非常感激您!手機(jī)微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
