djangodjango-ormdjango-database

Django: write something to the database in a catch block, when using an atomic transaction


I have a Django REST Framework serializer which uses select_for_update in combination with atomic transitions, like this: https://docs.djangoproject.com/en/4.2/ref/models/querysets/#select-for-update/. That works fine, except that I want to write something to the database when an error is thrown... and these insert statements are getting rolled back, never making it to the database.

The code is something like this (very much simplified but gets the point across and is reproducible):

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)

The problem I am facing is that when the external SDK triggers an error and I'm trying to write something to the database, that this never ends up in the database, the transaction is just rolled back.

How can I make sure that I can still write something to the database when using atomic transactions, in the except block?


Solution

  • Instead of decorator you can use context manager. For example:

    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)