Fixed #13055 -- Cleaned up the implementation of transaction decorators to provide a consistent external facing API. Thanks to olb@nebkha.net for the report.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@12752 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Russell Keith-Magee 2010-03-10 13:13:57 +00:00
parent f74b9aec10
commit ca81ad4f9d
2 changed files with 83 additions and 41 deletions

View File

@ -257,79 +257,91 @@ def savepoint_commit(sid, using=None):
# DECORATORS # # DECORATORS #
############## ##############
def autocommit(func_or_using=None): def autocommit(using=None):
""" """
Decorator that activates commit on save. This is Django's default behavior; Decorator that activates commit on save. This is Django's default behavior;
this decorator is useful if you globally activated transaction management in this decorator is useful if you globally activated transaction management in
your settings file and want the default behavior in some view functions. your settings file and want the default behavior in some view functions.
""" """
def inner_autocommit(func, using=None): def inner_autocommit(func, db=None):
def _autocommit(*args, **kw): def _autocommit(*args, **kw):
try: try:
enter_transaction_management(managed=False, using=using) enter_transaction_management(managed=False, using=db)
managed(False, using=using) managed(False, using=db)
return func(*args, **kw) return func(*args, **kw)
finally: finally:
leave_transaction_management(using=using) leave_transaction_management(using=db)
return wraps(func)(_autocommit) return wraps(func)(_autocommit)
if func_or_using is None:
func_or_using = DEFAULT_DB_ALIAS # Note that although the first argument is *called* `using`, it
if callable(func_or_using): # may actually be a function; @autocommit and @autocommit('foo')
return inner_autocommit(func_or_using, DEFAULT_DB_ALIAS) # are both allowed forms.
return lambda func: inner_autocommit(func, func_or_using) if using is None:
using = DEFAULT_DB_ALIAS
if callable(using):
return inner_autocommit(using, DEFAULT_DB_ALIAS)
return lambda func: inner_autocommit(func, using)
def commit_on_success(func_or_using=None): def commit_on_success(using=None):
""" """
This decorator activates commit on response. This way, if the view function This decorator activates commit on response. This way, if the view function
runs successfully, a commit is made; if the viewfunc produces an exception, runs successfully, a commit is made; if the viewfunc produces an exception,
a rollback is made. This is one of the most common ways to do transaction a rollback is made. This is one of the most common ways to do transaction
control in web apps. control in web apps.
""" """
def inner_commit_on_success(func, using=None): def inner_commit_on_success(func, db=None):
def _commit_on_success(*args, **kw): def _commit_on_success(*args, **kw):
try: try:
enter_transaction_management(using=using) enter_transaction_management(using=db)
managed(True, using=using) managed(True, using=db)
try: try:
res = func(*args, **kw) res = func(*args, **kw)
except: except:
# All exceptions must be handled here (even string ones). # All exceptions must be handled here (even string ones).
if is_dirty(using=using): if is_dirty(using=db):
rollback(using=using) rollback(using=db)
raise raise
else: else:
if is_dirty(using=using): if is_dirty(using=db):
commit(using=using) commit(using=db)
return res return res
finally: finally:
leave_transaction_management(using=using) leave_transaction_management(using=db)
return wraps(func)(_commit_on_success) return wraps(func)(_commit_on_success)
if func_or_using is None:
func_or_using = DEFAULT_DB_ALIAS
if callable(func_or_using):
return inner_commit_on_success(func_or_using, DEFAULT_DB_ALIAS)
return lambda func: inner_commit_on_success(func, func_or_using)
def commit_manually(func_or_using=None): # Note that although the first argument is *called* `using`, it
# may actually be a function; @autocommit and @autocommit('foo')
# are both allowed forms.
if using is None:
using = DEFAULT_DB_ALIAS
if callable(using):
return inner_commit_on_success(using, DEFAULT_DB_ALIAS)
return lambda func: inner_commit_on_success(func, using)
def commit_manually(using=None):
""" """
Decorator that activates manual transaction control. It just disables Decorator that activates manual transaction control. It just disables
automatic transaction control and doesn't do any commit/rollback of its automatic transaction control and doesn't do any commit/rollback of its
own -- it's up to the user to call the commit and rollback functions own -- it's up to the user to call the commit and rollback functions
themselves. themselves.
""" """
def inner_commit_manually(func, using=None): def inner_commit_manually(func, db=None):
def _commit_manually(*args, **kw): def _commit_manually(*args, **kw):
try: try:
enter_transaction_management(using=using) enter_transaction_management(using=db)
managed(True, using=using) managed(True, using=db)
return func(*args, **kw) return func(*args, **kw)
finally: finally:
leave_transaction_management(using=using) leave_transaction_management(using=db)
return wraps(func)(_commit_manually) return wraps(func)(_commit_manually)
if func_or_using is None:
func_or_using = DEFAULT_DB_ALIAS # Note that although the first argument is *called* `using`, it
if callable(func_or_using): # may actually be a function; @autocommit and @autocommit('foo')
return inner_commit_manually(func_or_using, DEFAULT_DB_ALIAS) # are both allowed forms.
return lambda func: inner_commit_manually(func, func_or_using) if using is None:
using = DEFAULT_DB_ALIAS
if callable(using):
return inner_commit_manually(using, DEFAULT_DB_ALIAS)
return lambda func: inner_commit_manually(func, using)

View File

@ -56,17 +56,39 @@ Exception: I meant to do that
>>> Reporter.objects.all() >>> Reporter.objects.all()
[<Reporter: Alice Smith>, <Reporter: Ben Jones>] [<Reporter: Alice Smith>, <Reporter: Ben Jones>]
# With the commit_on_success decorator, the transaction is only comitted if the # the autocommit decorator also works with a using argument
>>> using_autocomitted_create_then_fail = transaction.autocommit(using='default')(create_a_reporter_then_fail)
>>> using_autocomitted_create_then_fail("Carol", "Doe")
Traceback (most recent call last):
...
Exception: I meant to do that
# Same behavior as before
>>> Reporter.objects.all()
[<Reporter: Alice Smith>, <Reporter: Ben Jones>, <Reporter: Carol Doe>]
# With the commit_on_success decorator, the transaction is only committed if the
# function doesn't throw an exception # function doesn't throw an exception
>>> committed_on_success = transaction.commit_on_success(create_a_reporter_then_fail) >>> committed_on_success = transaction.commit_on_success(create_a_reporter_then_fail)
>>> committed_on_success("Carol", "Doe") >>> committed_on_success("Dirk", "Gently")
Traceback (most recent call last): Traceback (most recent call last):
... ...
Exception: I meant to do that Exception: I meant to do that
# This time the object never got saved # This time the object never got saved
>>> Reporter.objects.all() >>> Reporter.objects.all()
[<Reporter: Alice Smith>, <Reporter: Ben Jones>] [<Reporter: Alice Smith>, <Reporter: Ben Jones>, <Reporter: Carol Doe>]
# commit_on_success decorator also works with a using argument
>>> using_committed_on_success = transaction.commit_on_success(using='default')(create_a_reporter_then_fail)
>>> using_committed_on_success("Dirk", "Gently")
Traceback (most recent call last):
...
Exception: I meant to do that
# This time the object never got saved
>>> Reporter.objects.all()
[<Reporter: Alice Smith>, <Reporter: Ben Jones>, <Reporter: Carol Doe>]
# If there aren't any exceptions, the data will get saved # If there aren't any exceptions, the data will get saved
>>> def remove_a_reporter(): >>> def remove_a_reporter():
@ -76,22 +98,22 @@ Exception: I meant to do that
>>> remove_comitted_on_success = transaction.commit_on_success(remove_a_reporter) >>> remove_comitted_on_success = transaction.commit_on_success(remove_a_reporter)
>>> remove_comitted_on_success() >>> remove_comitted_on_success()
>>> Reporter.objects.all() >>> Reporter.objects.all()
[<Reporter: Ben Jones>] [<Reporter: Ben Jones>, <Reporter: Carol Doe>]
# You can manually manage transactions if you really want to, but you # You can manually manage transactions if you really want to, but you
# have to remember to commit/rollback # have to remember to commit/rollback
>>> def manually_managed(): >>> def manually_managed():
... r = Reporter(first_name="Carol", last_name="Doe") ... r = Reporter(first_name="Dirk", last_name="Gently")
... r.save() ... r.save()
... transaction.commit() ... transaction.commit()
>>> manually_managed = transaction.commit_manually(manually_managed) >>> manually_managed = transaction.commit_manually(manually_managed)
>>> manually_managed() >>> manually_managed()
>>> Reporter.objects.all() >>> Reporter.objects.all()
[<Reporter: Ben Jones>, <Reporter: Carol Doe>] [<Reporter: Ben Jones>, <Reporter: Carol Doe>, <Reporter: Dirk Gently>]
# If you forget, you'll get bad errors # If you forget, you'll get bad errors
>>> def manually_managed_mistake(): >>> def manually_managed_mistake():
... r = Reporter(first_name="David", last_name="Davidson") ... r = Reporter(first_name="Edward", last_name="Woodward")
... r.save() ... r.save()
... # oops, I forgot to commit/rollback! ... # oops, I forgot to commit/rollback!
>>> manually_managed_mistake = transaction.commit_manually(manually_managed_mistake) >>> manually_managed_mistake = transaction.commit_manually(manually_managed_mistake)
@ -99,4 +121,12 @@ Exception: I meant to do that
Traceback (most recent call last): Traceback (most recent call last):
... ...
TransactionManagementError: Transaction managed block ended with pending COMMIT/ROLLBACK TransactionManagementError: Transaction managed block ended with pending COMMIT/ROLLBACK
# commit_manually also works with a using argument
>>> using_manually_managed_mistake = transaction.commit_manually(using='default')(manually_managed_mistake)
>>> using_manually_managed_mistake()
Traceback (most recent call last):
...
TransactionManagementError: Transaction managed block ended with pending COMMIT/ROLLBACK
""" """