Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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)
like image 381
alexpirine Avatar asked Feb 12 '13 12:02

alexpirine


People also ask

Can nested transaction be distributed?

A flat or nested transaction that accesses objects handled by different servers is referred to as a distributed transaction.

What are the rules to abort the nested transaction?

The rules to the usage of a nested transaction are as follows: While the nested (child) transaction is active, the parent transaction may not perform any operations other than to commit or abort, or to create more child transactions. Committing a nested transaction has no effect on the state of the parent transaction.

What are the advantages of nested transactions?

Advantages of Nested Transactions 1. Nested transactions allow for a simple composition of subtransactions improving modularity of the overall structure. 2. The concurrent execution of subtransactions that follow the prescribed rules allows for enhanced concurrency while preserving consistency.

How do I rollback a Django transaction?

Django doesn't provide an API to start a transaction. The expected way to start a transaction is to disable autocommit with set_autocommit() . Once you're in a transaction, you can choose either to apply the changes you've performed until this point with commit() , or to cancel them with rollback() .


Video Answer


2 Answers

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.

like image 187
Martin Larente Avatar answered Oct 30 '22 15:10

Martin Larente


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.

like image 21
alexpirine Avatar answered Oct 30 '22 15:10

alexpirine