如果你花费了很多的时间去进行Django数据库事务处理的话,你将会了解到这是让人晕头转向的。
在过去,只是提供了简单的基础文档,要想清楚知道它是怎么使用的,还必须要通过创建和执行Django的事务处理。
这里有众多的Django事务处理的名词,例如:commit_on_success , commit_manually , commit_unless_maneged,rollback_unless_managed,enter_transaction_management,leace_transaction_management,这些只是其中的一部分而已。
最让人感到幸运的是,Django 1.6版本发布后。现在你可以紧紧使用几个函数就可以实现事务处理了,在我们进入学习这些函数的几秒之前。首先,我们要明确下面这些问题:
什么是事务处理呢?
Django 1.6中的出现了那些新的事物处理优先权?
在进入“要怎么才是Django 1.6版本中正确的事务处理”之前,请先了解一下下面的详细案例:
带状案例
事务
推荐的方式
使用分隔符
每一个HTTP请求事务
保存点
嵌套的事务
根据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
所以一个事务是 数据库中单个的工作单元。它是由一个start transaction开始和一个commit或者显式的rollback结束。
为了完整地回答这个问题,我们必须阐述一下事务在数据库、客户端以及Django中是如何处理的。
数据库
数据库中的每一条语句都运行在一个事务中,这个事务甚至可以只包含一条语句。
几乎所有的数据库都有一个AUTOCOMMIT设置,通常它被默认设置为True。AUTOCOMMIT将所有语句包装到一个事务里,只要语句成功执行,这个事务就立即被提交。当然你也可以手动调用START_TRANSACTION,它会暂时将AUTOCOMMIT挂起,直到你调用COMMIT_TRANSACTION或者ROLLBACK。
然后,这种方式将会使AUTOCOMMIT设置的作用于每条语句后的隐式提交失效。
然而,有诸如像sqlite3和mysqldb的python客户端库,它允许python程序与数据库本身相连接。这些库遵循一套如何访问与查询数据库的标准。该DB API 2.0标准,被描述在PEP 249之中。虽然它可能让人阅读稍干一些。一个重要带走的是,在PEP 249状态之中,默认数据库应该关闭自动提交功能。
这明显与数据库内发生了什么矛盾冲突:
数据库语句总是要在一个事务中运行。此数据库一般会为你打开自动提交功能。
不过,根据PEP 249,这不应该发生。
客户端库必须反映在数据库之中发生了什么?但由于他们不允许默认打开自动提交功能。他们只是简单地在一个事务中包裹sql语句。就像数据库。
好啦,在我身边呆久一些吧。
进入Django,Django也有关于事务处理的话要说。在Django 1.5和更早的版本。当你写数据到数据库时,Django基本上是运行一个开放的事务和自动提交该事务功能。所以每次你所称谓的像诸如model.save() 或者model.update()的东西,Django生成相应的sql语句,并提交该事务。
也有在Django 1.5和更早的版本,它是建议你使用TransactionMiddleware绑定http请求事务。每个请求提供了一个事务。如果返回的响应没有异常,Django会提交此事务。但如果你的视图功能抛出一个错误,回滚将被调用。这实际上说明,它关闭了自动提交功能。如果你想要标准化,数据库级别自动提交风格式的事务管理,你必须管理你自己的交易-通常是通过使用事务装饰你的视图功能,例如@transaction.commit_manually,或者@transaction.commit_on_success.
吸一口气,或者两口。
下面,我们使用处理一个用户注册的例子,调用了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已关闭),我们将用新创建用户的email向UnpaidUsers表增加一个新条目,这样我们可以让他们稍后重试他们信用卡信息。
思路是这样的:如果Stripe没有响应,用户依然可以注册,然后开始使用我们的网站。我们将在稍后的时候让用户提供信用卡的信息。
“我明白这是一个特殊的例子,并且这也不是我想完成的功能的方式,但是它的目的是展示交易”
考虑交易,牢记住在Django1.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' : '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)
当我们尝试去保存“UnpaidUsers”,测试上方的解释器就会跑出异常'IntegrityError' 。
接下来是解释这个问题的答案,“当“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="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)
所以如果我们运行了测试用例,那么它就该运行失败:
====================================================================== 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 ----------------------------------------------------------------------
赞。这就是我们最终想要的结果。
记住:我们这里已经练习了“测试驱动开发”的能力。错误信息提示我们:用户信息已经被保存到数据库中,但是这个并不是我们想要的,因为我们并没有付费!
事务交易用于挽救这样问题 ...
对于Django1.6,有很多种方式来创建事务。
这里简单介绍几种。
依据Django1.6的文档,“Django提供了一种简单的API去控制数据库的事务交易...原子操作用来定义数据库事务的属性。原子操作允许我们在数据库保证的前提下,创建一堆代码。如果这些代码被成功的执行,所对应的改变也会提交到数据库中。如果有异常发生,那么操作就会回滚。”
原子操作可以被用于解释操作或者是内容管理。所以如果我们用作为内容管理的时候,我们的注册函数的代码就会如下:
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”调用失败的时候,内容管理的所有操作都会被一起回滚。
评论删除后,数据将无法恢复
评论(1)