Django原子请求下事务回滚时如何持久化特定模型变更?
问题
我有一个开启了ATOMIC_REQUESTS=True的Django项目。当认证失败时,我需要存储登录尝试的关键数据,示例代码如下:
class LoginSerializer(serializers.Serializer): ... def validate(self, data): ... user = authenticate(email=email, password=password) if user is None: failed_login_attempt(email) raise serializers.ValidationError("Invalid credentials") def failed_login_attempt(email): user = User.objects.get(email=email) user.lock_status = "LOCKED" user.save() Device.objects.create(user=user, ip="127.0.0.1", fingerprint="some-fingerprint") Activity.objects.create(user=user, type="failed_login_attempt")
约束条件
- 抛出
ValidationError会导致所有变更(包括对user.lock_status的更新、Device和Activity的创建)被回滚。这种行为是全局事务的正常表现,但我需要这些变更能够持久化。 - 无法关闭
ATOMIC_REQUESTS=True,因为它对应用的其他部分至关重要。
目标
确保即使ValidationError回滚了其余事务,failed_login_attempt函数中的变更仍能持久化。
已尝试的方法
- 将
failed_login_attempt包裹在transaction.atomic()中:此方法无效,因为它仍属于外部事务的一部分。 - 配置独立数据库并设置
ATOMIC_REQUESTS=False,搭配自定义Database Router,但存在问题:- 需要为其他模型(User、Device)设置广泛权限,这会允许在全局事务外进行写入操作;
- 无法仅将此方法限定在
failed_login_attempt函数中,导致控制范围过宽。
- 绕过ORM使用原生SQL的
connections.cursor()感觉像是权宜之计,因此希望避免这种方式。
解决方案
最佳方案是在failed_login_attempt函数中使用**transaction.autocommit()上下文管理器**,让该函数内的数据库操作脱离全局事务的控制,直接提交到数据库,不受外部事务回滚的影响。
修改后的failed_login_attempt函数代码如下:
from django.db import transaction def failed_login_attempt(email): # 使用autocommit上下文,确保操作立即提交,不受外部事务影响 with transaction.autocommit(): user = User.objects.get(email=email) user.lock_status = "LOCKED" user.save() Device.objects.create(user=user, ip="127.0.0.1", fingerprint="some-fingerprint") Activity.objects.create(user=user, type="failed_login_attempt")
原理说明
- 当
ATOMIC_REQUESTS=True时,Django会为每个请求自动开启一个全局事务,所有数据库操作默认归属这个事务,一旦请求抛出异常,整个事务会被回滚。 transaction.autocommit()上下文管理器会临时关闭当前事务管理,块内的每个数据库操作都会直接提交到数据库,完全独立于外部全局事务。即使后续抛出ValidationError导致全局事务回滚,这里已提交的变更也不会被撤销。
替代方案:使用transaction.atomic()配合savepoint=False
如果需要保证failed_login_attempt内操作的原子性(要么全部成功,要么全部失败),可以用transaction.atomic()并设置savepoint=False,强制创建独立的顶级事务,而非外部事务的保存点:
from django.db import transaction def failed_login_attempt(email): with transaction.atomic(savepoint=False): user = User.objects.get(email=email) user.lock_status = "LOCKED" user.save() Device.objects.create(user=user, ip="127.0.0.1", fingerprint="some-fingerprint") Activity.objects.create(user=user, type="failed_login_attempt")
这个方案同样能让函数内的事务独立于全局事务,执行完成后立即提交,不会被后续全局事务的回滚影响。
内容的提问来源于stack exchange,提问作者an0o0nym
相关产品推荐
相关产品推荐

