DBrowne
DBrowne

Reputation: 703

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?

Upvotes: 2

Views: 1201

Answers (1)

DBrowne
DBrowne

Reputation: 703

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.

Upvotes: 1

Related Questions