alexpirine
alexpirine

Reputation: 3263

Working with nested @transaction.commit_on_success in Django

Consider this simple example :

# a bank account class
class Account:
    @transaction.commit_on_success
    def withdraw(self, amount):
        # code to withdraw money from the account 

    @transaction.commit_on_success
    def add(self, amount):
        # code to add money to the account

# somewhere else
@transaction.commit_on_success
def makeMoneyTransaction(src_account, dst_account, amount):
    src_account.withdraw(amount)
    dst_account.add(amount)

(taken from https://code.djangoproject.com/ticket/2227)

If an exception raises in Account.add(), the transaction in Account.withdraw() will still be committed and money will be lost, because Django doesn't currently handle nested transactions.

Without applying patchs to Django, how can we make sure that the commit is sent to the database, but only when the main function under the @transaction.commit_on_success decorator finishes without raising an exception?

I came across this snippet: http://djangosnippets.org/snippets/1343/ and it seems like it could do the job. Is there any drawbacks I should be aware of if I use it?

Huge thanks in advance if you can help.

P.S. I am copying the previously cited code snippet for purposes of visibility:

def nested_commit_on_success(func):
    """Like commit_on_success, but doesn't commit existing transactions.

    This decorator is used to run a function within the scope of a 
    database transaction, committing the transaction on success and
    rolling it back if an exception occurs.

    Unlike the standard transaction.commit_on_success decorator, this
    version first checks whether a transaction is already active.  If so
    then it doesn't perform any commits or rollbacks, leaving that up to
    whoever is managing the active transaction.
    """
    commit_on_success = transaction.commit_on_success(func)
    def _nested_commit_on_success(*args, **kwds):
        if transaction.is_managed():
            return func(*args,**kwds)
        else:
            return commit_on_success(*args,**kwds)
    return transaction.wraps(func)(_nested_commit_on_success)

Upvotes: 7

Views: 3952

Answers (2)

Martin Larente
Martin Larente

Reputation: 520

The problem with this snippet is that it doesn't give you the ability to roll back an inner transaction without rolling back the outer transaction as well. For example:

@nested_commit_on_success
def inner():
    # [do stuff in the DB]

@nested_commit_on_success
def outer():
    # [do stuff in the DB]
    try:
        inner()
    except:
        # this did not work, but we want to handle the error and
        # do something else instead:

        # [do stuff in the DB]

outer()

In the example above, even if inner() raises an exception, its content won't be rolled back.

What you need is a savepoint for the inner "transactions". For your code, it might look like this:

# a bank account class
class Account:
    def withdraw(self, amount):
        sid = transaction.savepoint()
        try:
            # code to withdraw money from the account
        except:
            transaction.savepoint_rollback(sid)
            raise

    def add(self, amount):
        sid = transaction.savepoint()
        try:
            # code to add money to the account
        except:
            transaction.savepoint_rollback(sid)
            raise

# somewhere else
@transaction.commit_on_success
def makeMoneyTransaction(src_account, dst_account, amount):
    src_account.withdraw(amount)
    dst_account.add(amount)

As of Django 1.6, the atomic() decorator does exactly that: it uses a transaction for the outer use of the decorator, and any inner use uses a savepoint.

Upvotes: 6

alexpirine
alexpirine

Reputation: 3263

Django 1.6 introduces @atomic, which does exactly what I was looking for!

Not only it supports "nested" transactions, but it also replaces the old, less powerful, decorators. And it is good to have a unique and consistent behavior for transactions management across different Django apps.

Upvotes: 2

Related Questions