事务
对于Django1.6,有很多种方式来创建事务。
这里简单介绍几种。
推荐的方法
依据Django1.6的文档,“Django提供了一种简单的API去控制数据库的事务交易...原子操作用来定义数据库事务的属性。原子操作允许我们在数据库保证的前提下,创建一堆代码。如果这些代码被成功的执行,所对应的改变也会提交到数据库中。如果有异常发生,那么操作就会回滚。”
原子操作可以被用于解释操作或者是内容管理。所以如果我们用作为内容管理的时候,我们的注册函数的代码就会如下:
-
from django.db
import transaction
-
-
-
with transaction.atomic():
-
user = User.create(cd[
'name'], cd[
'email'], cd[
'password'], cd[
'last_4_digits'])
-
-
-
user.stripe_id = customer.id
-
-
-
UnpaidUsers(email=cd[
'email']).save()
-
-
-
form.addError(cd[
'email'] +
' is already a member')
注意在“with transaction.atomic()”这一行。这块代码将会在事务内部执行。所以如果我们重新运行了我们的测试,他们都将会通过。
记住:事务是一个工作单元,所以当“UnpaidUsers”调用失败的时候,内容管理的所有操作都会被一起回滚。
使用装饰器
除了上面的做法,我们能使用Python的装饰器特性来使用事务。
-
-
-
-
-
-
user = User.create(cd[
'name'], cd[
'email'], cd[
'password'], cd[
'last_4_digits'])
-
-
-
user.stripe_id = customer.id
-
-
-
UnpaidUsers(email=cd[
'email']).save()
-
-
-
form.addError(cd[
'email'] +
' is already a member')
如果我们重新跑一次测试,那还是会一样失败。
为啥呢?为啥事务没有正确回滚呢?原因在与transaction.atomic会尝试捕获某种异常,而我们代码里人肉捕抓了(例如 try-except 代码块里的IntegrityError 异常),所以 transaction.atomic 永远看不到这个异常,所以标准的AUTOCOMMIT 流程就此无效掉。
但是,删掉try-catch语句会导致异常没有捕获,然后代码流程十有八九会就此乱掉。所以啊,也就是说不能去掉try-catch。
所以,技巧是将原子上下文管理器放入我们在第一个解决方案中的 try-catch 代码段里。
再看一下正确的代码:
-
from django.db
import transaction
-
-
-
with transaction.atomic():
-
user = User.create(cd[
'name'], cd[
'email'], cd[
'password'], cd[
'last_4_digits'])
-
-
-
user.stripe_id = customer.id
-
-
-
UnpaidUsers(email=cd[
'email']).save()
-
-
-
form.addError(cd[
'email'] +
' is already a member')
当 UnpaidUsers 触发 IntegrityError 时,上下文管理器 transaction.atomic() 会捕获到它,并执行回滚操作。此时我们的代码在异常处理中执行(即行 theform.addErrorline),将会完成回滚操作,并且,如果必要的话,也可以安全的进行数据库调用。也要注意:任何在上下文管理器 thetransaction.atomic() 前后的数据库调用都不会受到它的执行结果的影响。
针对每次HTTP请求的事务交易
Django1.5和1.6版本都允许用户操作请求事务模式。在这种模式下,Django会自动在事务中,处理你的视图函数。如果视图函数抛出异常,Django会自动回滚事务;否则Django会提交事务。
为了实现这个功能,你需要在你想要有此功能的数据库的配置中,设置“ATOMIC_REQUEST”为真。所以在我们的“settings.py”需要有如下设置:
-
-
-
'ENGINE':
'django.db.backends.sqlite3',
-
'NAME': os.path.join(SITE_ROOT,
'test.db'),
-
-
-
如果我们把解释器放到视图函数中,以上设置就会生效。所以这并没有符合我们的想法。
但是,这里依然值得注意的是解释器:“ATOMIC_REQUESTS”和“@transaction.atomic”仍然会有可能在有异常抛出的时候,处理这些错误。为了去捕捉这些错误,你需要去完成一些常规的中间件,或者需要去覆盖“urls.hadler500”,或者是创建新的“500.html”模板。
保存点
尽管事务是有原子性的,但还是能够打散为多个“保存点”——你可看作是“部分事务”。
例如,你有个事务包含了4条SQL语句,你可以在第二个SQL之后创建一个保存点。一旦保存点创建成功,就算第三条或第四条SQL执行失败,你仍旧能够做一个部分回滚,忽视后面两条SQL,仅保留前面两条。
基本上,这就像是提供了一个切割的能力:一个普通的事务能够被切分为多个更“轻量”的事务,然后能进行部分回滚或部分提交。
但一定要注意,小心整个事务被无端回滚掉(例如由于抛出了IntegrityError异常但却没有捕抓,那所有的保存点都会因此被回滚掉的)
来看个示例代码,了解怎么玩转保存点。
-
-
def save_points(self,save=True):
-
-
user = User.create(
'jj',
'inception',
'jj',
'1234')
-
sp1 = transaction.savepoint()
-
-
user.name =
'zheli hui guadiao, T.T'
-
-
-
-
-
transaction.savepoint_commit(sp1)
-
-
transaction.savepoint_rollback(sp1)
示例中,整个函数都是属于一个事务的。新User对象创建后,我们创建并得到了一个保存点。然后后续3行语句——
-
user.name =
'zheli hui guadiao, T.T'
-
-
——不属于刚才的保存点,因此他们有可能是属于下面的savepoint_rollback或savepoint_commit的一部分。假设是savepoint_rollback, 那代码行user = User.create('jj','inception','jj','1234')仍旧会成功提交到数据库中 ,而下面三行则不会成功。
采用另外一种方法,下面的两种测试用例描述了保存点是如何工作的:
-
def test_savepoint_rollbacks(self):
-
-
-
-
-
users = User.objects.filter(email=
"inception")
-
self.assertEquals(len(users),
1)
-
-
-
self.assertEquals(users[
0].stripe_id,
'')
-
self.assertEquals(users[
0].name,
'jj')
-
-
-
def test_savepoint_commit(self):
-
-
-
-
users = User.objects.filter(email=
"inception")
-
self.assertEquals(len(users),
1)
-
-
-
self.assertEquals(users[
0].stripe_id,
'4')
-
self.assertEquals(users[
0].name,
'starting down the rabbit hole')
同样,在我们提交或者回滚保存点之后,我们仍然可以继续在同一个事务中工作。同时,这个运行结果不受之前保存点输出结果的影响。
例如,如果我们按照如下例子更新“save_points”函数,
-
-
def save_points(self,save=True):
-
-
user = User.create(
'jj',
'inception',
'jj',
'1234')
-
sp1 = transaction.savepoint()
-
-
user.name =
'starting down the rabbit hole'
-
-
-
-
-
-
-
transaction.savepoint_commit(sp1)
-
-
transaction.savepoint_rollback(sp1)
-
-
user.create(
'limbo',
'illbehere@forever',
'mind blown',
-
即使无论是“savepoint_commit”或者“savepoint_rollback”被“limbo”这个用户调用了,这个事务仍然会被成功创建。如果没有创建成功,整个事务将会被回滚。
嵌套事务
采用“savepoint()”,“savepoint_commit”和“savepoint_rollback”去手动指定保存点,将会自动一个嵌套事务,同时这个嵌套事务会自动为我们创建一个保存点。并且,如果我们遇到错误,这个事务将会回滚。
下面用一个扩展的例子来说明:
-
-
def save_points(self,save=True):
-
-
user = User.create(
'jj',
'inception',
'jj',
'1234')
-
sp1 = transaction.savepoint()
-
-
user.name =
'starting down the rabbit hole'
-
-
-
-
-
-
-
transaction.savepoint_commit(sp1)
-
-
transaction.savepoint_rollback(sp1)
-
-
-
with transaction.atomic():
-
user.create(
'limbo',
'illbehere@forever',
'mind blown',
-
-
if
not save:
raise DatabaseError
-
-
这里我们可以看到:在我们处理保存点之后,我们采用“thetransaction.atomic”的上下文管理区擦出我们创建的"limbo"这个用户。当上下文管理被调用的时候,它会创建一个保存点(因为我们已经在事务里面了),同时这个保存点将会依据已经存在的上下文管理器去被执行或者回滚。
这样下面两个测试用例就描述了这个行文:
-
def test_savepoint_rollbacks(self):
-
-
-
-
-
users = User.objects.filter(email=
"inception")
-
self.assertEquals(len(users),
1)
-
-
-
self.assertEquals(users[
0].stripe_id,
'')
-
self.assertEquals(users[
0].name,
'jj')
-
-
-
limbo = User.objects.filter(email=
"illbehere@forever")
-
self.assertEquals(len(limbo),
0)
-
-
def test_savepoint_commit(self):
-
-
-
-
users = User.objects.filter(email=
"inception")
-
self.assertEquals(len(users),
1)
-
-
-
self.assertEquals(users[
0].stripe_id,
'4')
-
self.assertEquals(users[
0].name,
'starting down the rabbit hole')
-
-
-
limbo = User.objects.filter(email=
"illbehere@forever")
-
self.assertEquals(len(limbo),
1)
因此,在现实之中你可以使用原子或者在事务之中创建保存点的保存点。使用原子,你不必要很仔细地担心提交和会滚,当这种情况发生时,你可以完全控制其中的保存点。