Search code examples
djangotransactionsdatabase-locking

Using django select_for_update without rolling back on error


I'm trying to utilize django's row-level-locking by using the select_for_update utility. As per the documentation, this can only be used when inside of a transaction.atomic block. The side-effect of using a transaction.atomic block is that if my code throws an exception, all the database changes get rolled-back. My use case is such that I'd actually like to keep the database changes, and allow the exception to propagate. This leaves me with code looking like this:

with transaction.atomic():
    user = User.objects.select_for_update.get(id=1234)
    try:
        user.do_something()
    except Exception as e:
        exception = e
    else:
        exception = None

if exception is not None:
    raise exception

This feels like a total anti-pattern and I'm sure I must be missing something. I'm aware I could probably roll-my-own solution by manually using transaction.set_autocommit to manage the transaction, but I'd have thought that there would be a simpler way to get this functionality. Is there a built in way to achieve what I want?


Solution

  • I ended up going with something that looks like this:

    from django.db import transaction
    
    class ErrorTolerantTransaction(transaction.Atomic):
    
        def __exit__(self, exc_type, exc_value, traceback):
            return super().__exit__(None, None, None)
    
    
    def error_tolerant_transaction(using=None, savepoint=True):
        """
        Wraps a code block in an 'error tolerant' transaction block to allow the use of
        select_for_update but without the effect of automatic rollback on exception.
    
        Can be invoked as either a decorator or context manager.
        """
        if callable(using):
            return ErrorTolerantTransaction('default', savepoint)(using)
    
        return ErrorTolerantTransaction(using, savepoint)
    

    I can now put an error_tolerant_transaction in place of transaction.atomic and exceptions can be raised without a forced rollback. Of course database-related exceptions (i.e. IntegrityError) will still cause a rollback, but that's expected behavior given that we're using a transaction. As a bonus, this solution is compatible with transaction.atomic, meaning it can be nested inside an atomic block and vice-versa.