Django中的每会话事务
我正在制作一个Django的网页应用,允许用户在一系列的GET和POST请求中逐步建立一组更改,然后通过最后一次POST将这些更改提交到数据库(或者选择撤销)。在这些更改被确认之前,我需要确保这些更新不会被其他同时使用数据库的用户看到(因为这是一个配置前端),所以不能在每次POST后就提交。
我比较喜欢的解决方案是使用每个会话的事务。这种方式可以把记住哪些地方改了(以及这些改动如何影响后续查询)和实现提交/回滚的所有问题都放在数据库里处理,这样比较合适。因为外部限制的原因,系统在任何时候只能有一个用户在配置,所以死锁和长时间持有锁的问题并不存在,而且这个用户的行为也很规范。
不过,我找不到关于如何设置Django的ORM来使用这种事务模型的文档。我自己拼凑了一个简单的解决方案(虽然不太好),但我不喜欢这种脆弱的做法。有没有人之前做过类似的事情?我是不是漏掉了什么文档?
(我使用的Django版本是1.0.2 Final,数据库是Oracle。)
3 个回答
我想分享一个类似于备忘录模式的想法,但又有些不同,所以觉得值得发出来。当用户开始编辑时,我会把目标对象复制到数据库中的一个临时对象里。之后的所有编辑操作都是在这个复制的对象上进行的。我不是在每次改变时都保存对象的状态,而是保存操作对象。当我对一个对象应用某个操作时,它会返回一个“反向”操作,我把这个反向操作存起来。
保存操作比保存备忘录要便宜得多,因为操作只需要几个小数据项来描述,而被编辑的对象通常要大得多。而且我在进行操作时就应用这些操作,并保存撤销操作,这样数据库中的临时对象总是和用户浏览器中的版本对应。我不需要重放一系列的变化;临时对象总是只差一个操作就能变成下一个版本。
要实现“撤销”功能,我会从操作栈中取出最后一个撤销对象(可以理解为从数据库中获取临时对象的最新操作),然后把它应用到临时对象上,返回变换后的临时对象。如果我想实现重做功能,我也可以把结果操作放到重做栈里。
要实现“保存更改”,也就是提交,我会停用并加上时间戳给原始对象,然后把临时对象激活替代它。
要实现“取消”,也就是回滚,我什么都不做!当然,我可以删除临时对象,因为一旦编辑会话结束,用户就无法找回它,但我喜欢保留取消的编辑会话,这样我可以在清理之前对它们进行统计分析。
如果有其他人遇到和我一样的问题(我希望不会),这里有我的一个临时解决方案。这个方法不太稳定,看起来也不太好,而且还修改了私有方法,但幸运的是它很小。请不要随便使用,除非真的没办法。正如其他人提到的,使用这个方法的应用程序会阻止多个用户同时进行更新,否则可能会导致死锁。(在我的应用中,可能有很多读者,但故意不允许多个同时更新。)
我有一个“用户”对象,它在用户会话中保持不变,并且包含一个持久的连接对象。当我验证某个HTTP交互是会话的一部分时,我还会把用户对象存储在django.db.connection上,这个连接是线程本地的。
def monkeyPatchDjangoDBConnection():
import django.db
def validConnection():
if django.db.connection.connection is None:
django.db.connection.connection = django.db.connection.user.connection
return True
def close():
django.db.connection.connection = None
django.db.connection._valid_connection = validConnection
django.db.connection.close = close
monkeyPatchDBConnection()
def setUserOnThisThread(user):
import django.db
django.db.connection.user = user
这个最后一步会在任何带有@login_required注解的方法开始时自动调用,所以我99%的代码都不需要关心这个临时解决方案的细节。
多个同时进行的会话级事务通常会导致死锁,或者更糟糕的情况(更糟糕的情况是活锁,即在另一个会话持有锁时长时间延迟)。
这种设计并不是最佳方案,这也是为什么Django不推荐这样做。
更好的解决方案如下。
设计一个Memento类,用来记录用户的更改。这可以是他们表单输入的保存副本。如果状态变化比较复杂,你可能需要记录额外的信息。否则,仅仅保存表单输入的副本可能就足够了。
在用户的会话中积累一系列的Memento对象。注意,事务的每一步都需要从数据中获取信息并验证这些Memento的链条是否仍然“有效”。有时候它们可能会失效,因为其他人改变了这个Memento链中的某些内容。那么该怎么办呢?
当你展示“准备提交吗?”的页面时,你已经回放了这系列的Memento,并且相当确定它们会有效。当用户点击“提交”时,你需要最后一次回放这些Memento,希望它们仍然有效。如果有效,那就太好了。如果无效,说明有人改变了某些东西,你又回到了第二步:那该怎么办呢?
这看起来很复杂。
是的,确实复杂。不过它不需要持有任何锁,这样可以实现非常快的速度,并且几乎没有死锁的机会。事务仅限于“提交”视图函数,这个函数实际上将这系列的Memento应用到数据库,保存结果,并最终提交以结束事务。
另一种做法——在用户在第n-1步时出去喝杯咖啡时持有锁——是不可行的。
想了解更多关于Memento的信息,可以查看这个链接。