如何在Django视图函数异常分支中更新模型并完成事务提交
问题根因
@transaction.non_atomic_requests装饰器仅对Django视图函数生效,你将其装饰在普通业务函数obtain_tokens上完全不生效。如果你的项目数据库配置开启了ATOMIC_REQUESTS = True(多数Django项目默认开启该配置),整个generate_auth视图会被包裹在全局事务中。- 内层
with transaction.atomic()块抛出AuthenticationFailed异常时,即便你在外层捕获了异常,整个外层事务也会被Django标记为「仅回滚」状态,后续所有数据库写操作都会被直接丢弃,哪怕你执行了attempt.save()也不会真正写入数据库。 - 你创建
AccessAttempt对象时,create方法已经自动执行了保存逻辑,后续额外调用的attempt.save()属于冗余操作。 - 原有视图代码里把返回状态的变量名和导入的
status模块重名,会导致变量覆盖的隐藏bug。
修复步骤
- 把
@transaction.non_atomic_requests装饰器移到views.py的视图函数上,移除obtain_tokens上的该装饰器,同时修复变量覆盖问题:
# views.py from django.db import transaction import json from django.http import JsonResponse from rest_framework import status @transaction.non_atomic_requests def generate_auth(request): b = json.loads(request.body) if b['username'] is None or b['password'] is None: return JsonResponse({'message': "Bad Request"}, status=status.HTTP_400_BAD_REQUEST) payload, res_status = obtain_tokens(request, b) return JsonResponse(payload, status=res_status)
- 修改
obtain_tokens函数的异常分支逻辑,更新失败次数时使用独立原子块,配合F表达式避免并发竞态问题:
# validate_jwt_auth.py from rest_framework_simplejwt.serializers import TokenObtainPairSerializer from .models.access_attempt import AccessAttempt from rest_framework import status from rest_framework.exceptions import AuthenticationFailed from django.db import transaction from django.db.models import F def obtain_tokens(request, credentials): attempt = AccessAttempt.objects.filter(username=credentials['username']).first() if attempt is None: # create方法已经自动保存,不需要额外调用save attempt = AccessAttempt.objects.create( username=credentials['username'], failures_since_start=0 ) if attempt.failures_since_start > 5: return {'message': "Failed too many times"}, status.HTTP_403_FORBIDDEN try: with transaction.atomic(): payload = TokenObtainPairSerializer(context={'request':request}).validate(credentials) attempt.failures_since_start = 0 attempt.save() return payload, status.HTTP_200_OK except AuthenticationFailed: # 独立事务更新失败次数,避免被内层异常导致的事务回滚标记影响 with transaction.atomic(): AccessAttempt.objects.filter(username=credentials['username']).update( failures_since_start=F('failures_since_start') + 1 ) return {'message': "Unauthorized"}, status.HTTP_401_UNAUTHORIZED
如果你不想开启非原子视图,也可以在内层异常捕获后先主动回滚事务再执行更新操作,效果一致:
except AuthenticationFailed: transaction.rollback() attempt.failures_since_start +=1 attempt.save() return {'message': "Unauthorized"}, status.HTTP_401_UNAUTHORIZED
内容的提问来源于stack exchange,提问作者everspader
相关产品推荐
相关产品推荐

