原标题:使用Django进行事务管理
Python部落组织翻译,禁止转载,欢迎转发
如果你曾经花大量时间学习过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请求的事务
存盘点
嵌套事务
什么是事务?
根据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设置,这个设置通常默认设置为真。这个AUTOCOMMIT覆盖了事务中的每一个语句,如果语句执行成功的话,就 会立即执行。当然了,你可以调用像START_TRANSACTION的语句,这个语句将会暂时停止AUTOCOMMIT,直到你调用 COMMIT_TRANSACTION或ROLLBACK。
但是,这里的缺点就是AUTOCOMMIT设置会在每一个语句之后应用一个隐含的执行。
客户端
有像sqlite3和mysqldb这样的客户端,可以将Python程序与数据库本身进行连接。这样的客户端具有一系列的用于获取和查询数据库的标准。 DB API 2.0标准在PEP 249中进行了描述。这个标准数据库可能阅读起来比较枯燥,一个重要的问题就是PEP 249表明数据库 AUTOCOMMIT应该是默认为OFF.
这就和数据中的一些情况发生了冲突:
SQL语句总是必须在事务中运行,数据库一般是通过AUTOCOMMIT对你开放。
然而,根据PEP 249,这个不应该发生。
客户端必须面对数据库中发生的情况,但是因为他们不允许将AUTOCOMMIT默认转换成on,他们就像在数据库中仅仅在事务中包括你的SQL语句。
好的。和我呆在一起久一点吧。
Django
进入到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语句,并且当需要的时候可以手动管理事务。从本质上讲,我们拥有一个非常简单的模型,基本上首先是做数据库是如何设计的。
理论已经足够了。让我们开始编写代码吧。
条纹示例
这里我们采用这个例子来观察函数如何处理登记用户和采用Strip进行信用卡处理。
def register(request):
user = None
if request.method == 'POST':
form = UserForm(request.POST)
if form.is_valid():
customer = Customer.create("subion",
email = form.cleaned_data['email'],
deion = 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来处理信用卡处理。然后我们创建一个新的用户。如果我们从Strip得到一个响 应,我们就采用strip_id更新新创建的客户。如果我们不能得到一个客户回应(Strip是向下的),我们将通过新创建的客户电子邮件增加一个 UnpaidUsers表的入口。因此我们可以要求它们稍后重新更新信用卡详情。
思想就是即使Strip是向下的,用户仍然可以登记和开始使用我们的网站。我们只是在后期再次要求他们信用卡信息。
我知道这可能是一个有点不自然的例子,并且这不是我实现这些功能的方式,但是目的是为了说明事务。
继续。考虑一下事务,并且记住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. 用户被创建(在数据库中),没有strip_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)
在测试顶部的装饰器是一个模拟,当我们尝试保存这个UnpaidUsers表时,将会抛出一个Integrity错误。
这就回答这个问题“那么UnpaidUsers(email=cd['email']).save() 这一行执行失败会发生什么呢?”接下来的一小点代码只是创建一个模拟会话,通过我们需要的合适的信息用于我们的注册函数。然后,with mock.patch迫使系统相信Stripe是向下的....最后我们进入到测试。
Resp = resister(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_st......
Traceback (most recent call last):
File "/Users/j1z0/..., line 1201, in patched
return func(*args, **keywargs)
File "/Users/j1z0/.....", line 266,
in test_registering_us.....othing
self.assertEquals(len(users), 0)
Asserti: 1 != 0
很好,看起来非常有趣,并且这也是我们想要的。记住,这里我们采用的是TDD。错误信息告诉我们用户确实存储在数据库中,这也的确不是我们想要的,因为他们不会支付!
通过事务来解决这个问题......
事务
在Django中实际上有很多方式来创建事务。
让我们来看一些。
推荐的方式
根据Django 1.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调用失败时,在上下文管理中的一切都会一起进行回滚。
使用装饰器
我们也可以尝试正价原子性作为装饰器。
@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')
如果我们重新运行我们的测试,他们会遇到我们之前相同的错误。
那是为什么呢?为什么那些事务不能正确的回滚呢?原因是因为transactions.atomic会寻找一些异常,我们捕捉到那个错误(比如在我们的 try except语句块中的IntergrityError),所以transaction.atomic不会解决它,因此标准的AUTOCOMMIT功能会 接管它。
但是,当然去掉try except将会导致异常抛出,并且很有可能会在其他地方发生。所以我们不能这么做。
所以技巧就是把原子的上下文管理放在我们做的第一个解决方案的try except块里。再次看一下正确的代码:
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.addErrot行),回滚将会完成,如果必要的话我们能够安全的进行数据库调用。
每个HTTP请求的事务
Django 1.6(和1.5一样)允许你以一种“每种请求的事务”的方式进行操作。在这种方式下,Django会自动在事务中包装你的回调函数。如果函数抛出了一个异常,Django将会回滚事务,否则它将会执行这个事务。
为了得到这个设置,你必须在每一个数据库设置中设置ATOMIC_REQUEST为真。所以在我们的“settting.py”中,我们修改如下:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(SITE_ROOT, 'test.db'),
'ATOMIC_REQUEST': True,
}
}
实际上,将装饰器放在回调函数中,这只是使行为更加准确。所以这里并没有说明我们的目的。
存盘点
即使事务是原子的,他们也可以进一步分解为存盘点。考虑将存盘点作为部分事务。
所以如果你有一个事务,执行4条SQL语句,你可以在第二个村盘点之后创建一个存盘点。一旦存盘点被创建了,即使第三条或者第四条语句执行失败了,你也可以进行部分回滚。去掉第3条和第4条语句但是保留前2条。
所以基本上可以分离一个事务成更小的事务,允许你进行部分回滚或执行。
让我们看一下存盘点如何工作的例子。
@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)
这里整个函数是在一个事务中。在创建一个新用户之后,我们创建了一个存盘点,并得到了这个存盘点的引用。接下来的3条语句:
user.name = 'starting down the rabbit hole'
user.stripe_id = 4
user.save()
进一步,下面二个测试描述了存盘点是如何工作的:
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被一个处于成功创建的不定状态的用户调用。除非别的情况引起了整个事务进行回滚。
嵌套事务
为了手动指定存盘点,通过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上下文管理来包装我们的不定状态用户的创建。当这个上下文管理被调用的时候,它实际上会创建一个存盘点,并且那个存盘点在退出上下文管理时将会执行或回滚。
因此,下面的两个测试描述了这种行为:
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是一个很好的例子,Django和Python都很骄傲进行表达。对于很多系统,你并不需要直接地解决事务,只需要让AUTOCOMMIT自己进行工作。但是,如果你这样做,希望这个帖子会给你在Django管理中需要的信息。返回搜狐,查看更多
责任编辑: