Django:当使用原子事务时,在catch块中向数据库写入一些内容



我有一个Django REST框架序列化器,它使用select_for_update与原子转换相结合,如下所示:https://docs.djangoproject.com/en/4.2/ref/models/querysets/#select-for-update/。这工作得很好,除了我想写一些东西到数据库当一个错误被抛出…这些插入语句被回滚,永远不会进入数据库。

代码是这样的(非常简化,但可以理解并可复制):

models.py

from django.db import models

class LicenseCode(models.Model):
code = models.CharField(max_length=200, unique=True)

class Activation(models.Model):
license_code = models.TextField(max_length=200)
activation_log = models.TextField(blank=True, null=True)
success = models.BooleanField(default=False)

views.py

from django.http import HttpResponse, Http404
from django.db import transaction
from .models import Activation, LicenseCode
class LicenseRedeemSerializer:
@transaction.atomic
def save(self):
license_codes = LicenseCode.objects.all().select_for_update()
activations = []
for license_code in license_codes:
activations.append(
Activation(license_code=license_code.code, success=False)
)
self.activate_licenses(activations)
def activate_licenses(self, activations):
try:
# In our real app we'd try to activate the licenses with an external SDK. This can fail.
raise Exception("Something went wrong!")
except Exception as e:
for activation in activations:
activation.activation_log = str(e)
activation.save()
# With our real DRF serializer we'd raise ValidationError
raise Http404("Could not activate the license!")

def view(request):
# Let's make sure we have a license code to work with
LicenseCode.objects.get_or_create(code="A")
serialier = LicenseRedeemSerializer()
serialier.save()
html = "Hello there"
return HttpResponse(html)

我面临的问题是,当外部SDK触发一个错误,我试图写一些东西到数据库,这永远不会结束在数据库中,事务只是回滚。

如何确保在except块中使用原子事务时仍然可以向数据库写入内容?

正如atomic()文档所提到的,它将在异常时回滚事务,因此我认为没有办法在错误期间直接将信息存储在Database中。

但是你总是可以在原子块之外捕获异常,然后保存错误并像这样重新引发错误:

class MySerializer(Serializer):
# Some serializer fields here..

def save(self):
try:
self.save_data()
except ValidationError as e:
Log.objects.create(error = str(e))
raise e
@transaction.atomic
def save_data(self):
foo = Foo.objects.all().select_for_update()
self.do_something(foo)
self.do_something_else(foo)
def do_something(self, foo):
try:
SomeExternalSDK.doStuff(foo)
except SomeExternalSDK.SomeException as e:
raise ValidationError(str(e))
def self.do_something_else(self, foo):
pass

或者,您可以创建一个对象变量(如列表),您可以将发生在SDK中的异常,然后稍后将这些错误存储在DB中。

通常,日志不存储在DB中,而是存储在文件中。你可以考虑将错误存储在文件系统中,或者使用Django的日志记录来存储错误,而不是将错误存储在数据库中。

更新

你可以移除允许回滚发生的嵌套异常引发。相反,您可以稍后使用标志来引发404。

@transaction.atomic
def save(self):
license_codes = LicenseCode.objects.all().select_for_update()
activations = []
for license_code in license_codes:
activations.append(
Activation(license_code=license_code.code, success=False)
)
error = self.activate_licenses(activations)  # using flag to see if there is any error
return error
def activate_licenses(self, activations):
has_error = False
try:
raise Exception("Something went wrong!")
except Exception as e:
for activation in activations:
activation.activation_log = str(e)
activation.save()
# flag
has_error = True
return has_error

#view
def view(request):
# Let's make sure we have a license code to work with
LicenseCode.objects.get_or_create(code="A")
serialier = LicenseRedeemSerializer()
error = serialier.save()
if error:
raise Http404('error activation')

或者,您可以查看保存点,但我不确定它如何适用于您的代码。

你可以使用上下文管理器来代替装饰器。例如:

from django.http import HttpResponse, Http404
from django.db import transaction
from .models import Activation, LicenseCode
class LicenseRedeemSerializer:
def save(self):
license_codes = LicenseCode.objects.all().select_for_update()
activations = []
with transaction.atomic():
for license_code in license_codes:
activations.append(
Activation(license_code=license_code.code, success=False)
)
self.activate_licenses(activations)
def activate_licenses(self, activations):
try:
# In our real app we'd try to activate the licenses with an external SDK. This can fail.
raise Exception("Something went wrong!")
except Exception as e:
for activation in activations:
activation.activation_log = str(e)
activation.save()
# With our real DRF serializer we'd raise ValidationError
raise Http404("Could not activate the license!")

def view(request):
# Let's make sure we have a license code to work with
LicenseCode.objects.get_or_create(code="A")
serialier = LicenseRedeemSerializer()
serialier.save()
html = "Hello there"
return HttpResponse(html)