1. 事务
本章我们将通过一个例子来简要的说明“事务”,这个开发实战里经常遇到的名词。事务是如何体现在一个具体的业务和系统的实现里。
事务是通过将一组相关操作组合为一个,要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。事务具有4个特性:原子性、一致性、隔离性、持久性。业务事务就是完成具体业务操作后,形成的业务结果;数据库事务是数据库产品根据事务的特性实现的相关功能,数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行,也可以理解成事务在数据库管理系统的实现。Django作为可以支持数据持久化的框架,也要支持的事务特性机制。
前面章节,我们描述的入库与库存的例子,现实世界的业务事务入库场景是这样,物品入库就是仓库管理员拿着新购进的物品,摆放到相应的货位上,同时更新库存登记簿,更新(增加)该物品的库存数据量,如果过程中仓库管理员忘记更新库存登记簿的库存数据将导致该物品的账目数据量与该物品的仓库货位实际库存数据量不一致,从而导致混乱。
库存管理业务系统依据这一业务逻辑规则,我们简化设计了2张表,入库单表和物品库存表来满足这一业务要求。业务系统的处理逻辑就是当新增的入库单数据保存到数据库入库表后,就必须更新库存表、该物品的库存数据,这个过程如果出现某种意外,如更新库存数据失败,就必须同时回滚、入库表的相应操作。这就是事务特性要求的:要么同时成功要么同时失败。
本章节将继续以入库事务这个例子来说明我们在系统中如何设计和实现事务的要求。
1.1. 入库操作流程
按照我们前面设计的入库单业务,每次入库单,其过程至少包括以下二个步数据库操作:
一、保存入库单信息到数据库;
二、更新入库单物品的当前库存信息,库存数量=当前库存数量+入库数量。
正常的情况下,这些操作将顺利进行,最终交易成功,与交易相关的所有数据库信息也成功地更新。但是,如果在这一系列过程中任何一个环节出了差错,例如在更新物品库存信息时发生异常导致交易失败。一旦交易失败,数据库中所有信息都必须保持交易前的状态不变,比如最后一步更新物品库存信息时失败而导致交易失败,那么必须保证这笔失败的交易不影响数据库的状态--库存信息没有被更新、入库单也没有提交成功。
1.1. 现在我们修改我们的代码来实现新增入库单时更新我们的库存信息
我们在AddInStockBill函数中增加如下代码来更新当前库存信息:
def AddInStockBill(request):
if request.method == 'POST':
form = InStockBillForm(request.POST)
if form.is_valid():
cd = form.cleaned_data
inStockBill = InStockBill()
inStockBill.InStockBillCode = cd['InStockBillCode']
inStockBill.InStockDate = cd['InStockDate']
inStockBill.Amount = cd['Amount']
inStockBill.Operator = cd['Operator']
inStockBill.Item = cd['Item'] inventorys = inStockBill.Item.inventory_set.all()
currentInventory = Inventory()
if (inventorys.count()==0):
currentInventory.Item = inStockBill.Item
currentInventory.Amount = inStockBill.Amount
else:
#这里假定只有一个物料,后面我们会根据进程重构代码
currentInventory = inventorys[0]
currentInventory.Amount = currentInventory.Amount + inStockBill.Amount currentInventory.save() #更新库存
inStockBill.save() #保存入库单数据
return HttpResponseRedirect('/success/')
else:
form = InStockBillForm() return render_to_response('InStockAdd.html',{'form': form}
,context_instance = RequestContext(request))
我们现在新增一条入库单来测试我们的入库事务是否实现了更新库存信息。
提交成功后,我们查看数据库会发现库存表和入库单表数据都保存成功了,如下图:
1.1. 事务失败
我们数据库inventory_instockbill表中增加一个非空字段remark来模拟,来模拟更新库存后,系统在提交入库单据时出现了异常系统返回失败了,由于model没有同步这个字段,入库单model提交时会引发错误,这时根据事务的规则,库存的更新应该会回滚,就是库存表数据不更新,入库单表没有新的入库单数据。如下图:
运行的结果,库存表库存数量更新为25,但是入库单据没有保存成功,也就是意味着系统运行的结果与业务事务是不符合的,丢失的入库单据已经导致库存数量发生变化,我们需要用一定的机制来保证业务事务满足要求。如下图:
1.2. Django事务处理
默认情况下,在Django中事务是自动提交的。当我们运行Django内置的模板修改函数时,例如调用model.save()或model.delete()时,事务将被立即提交。这种机制和数据库的自动提交事务机制类似。记住这里没有默认的回滚机制,要解决刚才的场景我们须引入Django的数据库事务控制类django.db.transaction
1.2.1. 在View中实现事务控制
如果想在更细粒度的条件下(例如在一个view函数中)控制事务,我们可以使用django.db.transaction。有两种用法:
1. 使用装饰器
from django.db import transaction @transaction.commit_on_success
def viewfunc(request):
# ... # this code executes inside a transaction # ...
2. 使用context manager
from django.db import transaction def viewfunc(request):
# ... # this code executes using default transaction management # ... with transaction.commit_on_success():
# ... # this code executes inside a transaction # ...
1.2.2. 标识使用方法
1. autocommit
使用autocommit装饰器可以将view函数中的事务还原成Django默认的自动提交模式,无视全局事务的设置。
from django.db import transaction @transaction.autocommit
def viewfunc(request): ....
2. commit_on_success()
顾名思义,view函数成功则提交事务,否则回滚。用法同上。
3. commit_manually()
告诉Django我们将自己控制函数中的事务处理。并且要注意,如果在视图函数中改变了数据库的数据并且没有调用commit()或rollback(),那么将抛出TransactionManagementError异常。
from django.db import transaction @transaction.commit_manually
def viewfunc(request): ...
# You can commit/rollback however and whenever you want
transaction.commit()
... # But you've got to remember to do it yourself! try:
... except: transaction.rollback() else: transaction.commit()
1.3. AddInStockBill增加事务标识
from django.db import transaction @transaction.commit_on_success
def AddInStockBill(request):
1.4. 事务测试
现在我们重新执行前面事务失败的例子,来看系统运行的结果是否满足事务的基本要求。
提交后系统报错,这我们也会发现数据库intentory表,提交的数据回滚了,库存数量没有更新。入库单业务实现了正确的业务事务,避免错误、混乱的数据提交到数据库中。
1.5. 小结
本章节我们用入库的业务例子来阐述如何运行Django的事务机制,以满足业务事务的要求,例子中我们采用了commit_on_success(),在实际应用中可以根据自己的业务逻辑采用不同的事务标识。
下一个章我们将继续以入库单的例子参数如何编写支持单元测试的代码例子。