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)
A flat or nested transaction that accesses objects handled by different servers is referred to as a distributed 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.
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.
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() .
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With