在 Django 中回滚数据库连接失败的正确方法

Proper way to rollback on DB connection fail in Django

这更像是一个设计问题。

直到最近,我一直在我的开发环境中使用带有 SQLite 的 Django,但我现在已更改为生产环境中的 PostgreSQL。我的应用程序是使用 Heroku 部署的,几天后我意识到他们对数据库进行了随机维护,并且它在几分钟内就宕机了。

例如,有一个包含 3 个表的模型,一个 Procedure 每个表指向一个 ProcedureList,而一个 ProcedureList 可以有多个 Procedure. ProcedureUser 链接 ProcedureList 和用户,并为 ProcedureList 上的用户设置一些特定变量。最后有一个 ProcedureStateProcedure 与其特定用户的状态相关联。

在我的应用程序中,在其中一个视图中,我有一个按以下方式修改数据库的函数:

user = request.user
plist = ProcedureList.objects.get(id=idFromUrl)
procedures = Procedure.objects.filter(ProcedureList=pList)

pUser = ProcedureUser(plist, user, someVariables)
pUser.save()

for procedure in procedures:
    pState = ProcedureState(plist, user, pUser, procedure, otherVariables)    
    pState.save()
    

所以我现在的想法是,如果 Heroku 决定在 object.save() 调用之间进行维护,我们就会遇到问题。稍后对 .save() 的调用将失败,数据库将被损坏。用户的请求当然会失败,也没有办法回滚之前的插入,因为无法与DB连接。

我的问题是,如果数据库出现故障(由 Heroku 维护、网络错误或其他原因引起),我们应该如何正确回滚数据库?要不要我们列个插入列表,等DB再上去回滚?

我正在使用 Python 3 和 Django 4,但我认为这是一个普遍问题,而不是特定于任何平台的问题。

in case of a DB fail (given by Heroku maintenance, network error or whatever), how are we supposed to correctly rollback the DB?

数据库通过atomic transactions [wiki]解决了这个问题。 atomic 事务是一组 allnone 提交的查询。因此,不可能对于此类事务,应用某些查询而其他查询不应用。

Django 提供了一个 transaction context manager [Django-doc] 来在事务中执行工作:

from django.db import <strong>transaction</strong>

with <strong>transaction.atomic()</strong>:
    user = request.user
    plist = ProcedureList.objects.get(id=idFromUrl)
    procedures = Procedure.objects.filter(ProcedureList=pList)
    
    pUser = ProcedureUser(plist, user, someVariables)
    pUser.save()
    
    ProcedureState.objects.bulk_create([
        ProcedureState(plist, user, pUser, procedure, otherVariables)
        for procedure in procedures
    ])

在上下文块的末尾,它将提交 更改。这意味着如果数据库在此期间发生故障,将不会提交操作,并且该块将引发异常(通常是 IntegrityError)。


Note: Django has a .bulk_create(…) method [Django-doc] to create multiple items with a single database query, minimizing the bandwidth between the database and the application layer. This will usually outperform creating items in a loop.