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 occurs in Account.add() , the transaction in Account.withdraw() will still be committed and money will be lost, because Django does not currently process nested transactions.

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

I came across this snippet: http://djangosnippets.org/snippets/1343/ , and it looks like he could do the job. Are there any flaws that I should know about if I use them?

Thank you so much in advance if you can help.

PS I copy the previously quoted code snippet for 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) 
+6
source share
2 answers

The problem with this snippet is that it does not give you the ability to roll back an internal transaction without rolling back from an external transaction. For instance:

 @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 above example, even if inner() throws an exception, its contents will not be undone.

For internal transactions you will need savepoint . 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 in Django 1.6, the atomic () decorator does just that: it uses a transaction to use the decorator externally and any internal use uses a savepoint.

+5
source

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

Not only does it support nested transactions, but it also replaces older, less powerful decorators. And it's nice to have unique and consistent behavior for managing transactions across different Django applications.

+2
source

All Articles