Fixed #10771 -- added support for using the transaction management functions as context managers in Python 2.5 and above. Thanks to Jacob for help with the docs.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14288 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Alex Gaynor 2010-10-19 19:38:15 +00:00
parent cfbba28c39
commit 27db9378cf
5 changed files with 333 additions and 134 deletions

View File

@ -11,6 +11,7 @@ called, a commit is made.
Managed transactions don't do those commits, but will need some kind of manual
or implicit commits or rollbacks.
"""
import sys
try:
import thread
@ -20,8 +21,9 @@ try:
from functools import wraps
except ImportError:
from django.utils.functional import wraps # Python 2.4 fallback.
from django.db import connections, DEFAULT_DB_ALIAS
from django.conf import settings
from django.db import connections, DEFAULT_DB_ALIAS
class TransactionManagementError(Exception):
"""
@ -257,32 +259,80 @@ def savepoint_commit(sid, using=None):
# DECORATORS #
##############
def autocommit(using=None):
class Transaction(object):
"""
Decorator that activates commit on save. This is Django's default behavior;
this decorator is useful if you globally activated transaction management in
your settings file and want the default behavior in some view functions.
"""
def inner_autocommit(func, db=None):
def _autocommit(*args, **kw):
try:
enter_transaction_management(managed=False, using=db)
managed(False, using=db)
return func(*args, **kw)
finally:
leave_transaction_management(using=db)
return wraps(func)(_autocommit)
Acts as either a decorator, or a context manager. If it's a decorator it
takes a function and returns a wrapped function. If it's a contextmanager
it's used with the ``with`` statement. In either event entering/exiting
are called before and after, respectively, the function/block is executed.
autocommit, commit_on_success, and commit_manually contain the
implementations of entering and exiting.
"""
def __init__(self, entering, exiting, using):
self.entering = entering
self.exiting = exiting
self.using = using
def __enter__(self):
self.entering(self.using)
def __exit__(self, exc_type, exc_value, traceback):
self.exiting(exc_value, self.using)
def __call__(self, func):
@wraps(func)
def inner(*args, **kwargs):
# Once we drop support for Python 2.4 this block should become:
# with self:
# func(*args, **kwargs)
self.__enter__()
try:
res = func(*args, **kwargs)
except:
self.__exit__(*sys.exc_info())
raise
else:
self.__exit__(None, None, None)
return res
return inner
def _transaction_func(entering, exiting, using):
"""
Takes 3 things, an entering function (what to do to start this block of
transaction management), an exiting function (what to do to end it, on both
success and failure, and using which can be: None, indiciating using is
DEFAULT_DB_ALIAS, a callable, indicating that using is DEFAULT_DB_ALIAS and
to return the function already wrapped.
Returns either a Transaction objects, which is both a decorator and a
context manager, or a wrapped function, if using is a callable.
"""
# 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_autocommit(using, DEFAULT_DB_ALIAS)
return lambda func: inner_autocommit(func, using)
return Transaction(entering, exiting, DEFAULT_DB_ALIAS)(using)
return Transaction(entering, exiting, using)
def autocommit(using=None):
"""
Decorator that activates commit on save. This is Django's default behavior;
this decorator is useful if you globally activated transaction management in
your settings file and want the default behavior in some view functions.
"""
def entering(using):
enter_transaction_management(managed=False, using=using)
managed(False, using=using)
def exiting(exc_value, using):
leave_transaction_management(using=using)
return _transaction_func(entering, exiting, using)
def commit_on_success(using=None):
"""
This decorator activates commit on response. This way, if the view function
@ -290,38 +340,23 @@ def commit_on_success(using=None):
a rollback is made. This is one of the most common ways to do transaction
control in Web apps.
"""
def inner_commit_on_success(func, db=None):
def _commit_on_success(*args, **kw):
try:
enter_transaction_management(using=db)
managed(True, using=db)
try:
res = func(*args, **kw)
except:
# All exceptions must be handled here (even string ones).
if is_dirty(using=db):
rollback(using=db)
raise
else:
if is_dirty(using=db):
try:
commit(using=db)
except:
rollback(using=db)
raise
return res
finally:
leave_transaction_management(using=db)
return wraps(func)(_commit_on_success)
def entering(using):
enter_transaction_management(using=using)
managed(True, using=using)
# 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 exiting(exc_value, using):
if exc_value is not None:
if is_dirty(using=using):
rollback(using=using)
else:
if is_dirty(using=using):
try:
commit(using=using)
except:
rollback(using=using)
raise
return _transaction_func(entering, exiting, using)
def commit_manually(using=None):
"""
@ -330,22 +365,11 @@ def commit_manually(using=None):
own -- it's up to the user to call the commit and rollback functions
themselves.
"""
def inner_commit_manually(func, db=None):
def _commit_manually(*args, **kw):
try:
enter_transaction_management(using=db)
managed(True, using=db)
return func(*args, **kw)
finally:
leave_transaction_management(using=db)
def entering(using):
enter_transaction_management(using=using)
managed(True, using=using)
return wraps(func)(_commit_manually)
def exiting(exc_value, using):
leave_transaction_management(using=using)
# 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_manually(using, DEFAULT_DB_ALIAS)
return lambda func: inner_commit_manually(func, using)
return _transaction_func(entering, exiting, using)

View File

@ -73,6 +73,19 @@ you just won't get any of the nice new unittest2 features.
.. _unittest2: http://pypi.python.org/pypi/unittest2
Transaction context managers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Users of Python 2.5 and above may now use :ref:`transaction management functions
<transaction-management-functions>` as `context managers`_. For example::
with transaction.autocommit():
# ...
.. _context managers: http://docs.python.org/glossary.html#term-context-manager
For more information, see :ref:`transaction-management-functions`.
Everything else
~~~~~~~~~~~~~~~

View File

@ -2,7 +2,7 @@
Managing database transactions
==============================
.. currentmodule:: django.db
.. currentmodule:: django.db.transaction
Django gives you a few ways to control how database transactions are managed,
if you're using a database that supports transactions.
@ -50,105 +50,138 @@ An exception is ``CacheMiddleware``, which is never affected. The cache
middleware uses its own database cursor (which is mapped to its own database
connection internally).
.. _transaction-management-functions:
Controlling transaction management in views
===========================================
For most people, implicit request-based transactions work wonderfully. However,
if you need more fine-grained control over how transactions are managed, you
can use Python decorators to change the way transactions are handled by a
particular view function. All of the decorators take an option ``using``
parameter which should be the alias for a database connection for which the
behavior applies to. If no alias is specified then the ``"default"`` database
is used.
.. versionchanged:: 1.3
Transaction management context managers are new in Django 1.3.
.. note::
For most people, implicit request-based transactions work wonderfully. However,
if you need more fine-grained control over how transactions are managed, you can
use a set of functions in ``django.db.transaction`` to control transactions on a
per-function or per-code-block basis.
These functions, described in detail below, can be used in two different ways:
* As a decorator_ on a particular function. For example::
from django.db import transaction
@transaction.commit_on_success()
def viewfunc(request):
# ...
# this code executes inside a transaction
# ...
This technique works with all supported version of Python (that is, with
Python 2.4 and greater).
* As a `context manager`_ around a particular block of code::
from django.db import transaction
def viewfunc(request):
# ...
# this code executes using default transaction management
# ...
with transaction.commit_on_success():
# ...
# this code executes inside a transaction
# ...
The ``with`` statement is new in Python 2.5, and so this syntax can only
be used with Python 2.5 and above.
.. _decorator: http://docs.python.org/glossary.html#term-decorator
.. _context manager: http://docs.python.org/glossary.html#term-context-manager
For maximum compatibility, all of the examples below show transactions using the
decorator syntax, but all of the follow functions may be used as context
managers, too.
.. note::
Although the examples below use view functions as examples, these
decorators can be applied to non-view functions as well.
decorators and context managers can be used anywhere in your code
that you need to deal with transactions.
.. _topics-db-transactions-autocommit:
``django.db.transaction.autocommit``
------------------------------------
.. function:: autocommit
Use the ``autocommit`` decorator to switch a view function to Django's default
commit behavior, regardless of the global transaction setting.
Use the ``autocommit`` decorator to switch a view function to Django's
default commit behavior, regardless of the global transaction setting.
Example::
Example::
from django.db import transaction
from django.db import transaction
@transaction.autocommit
def viewfunc(request):
....
@transaction.autocommit
def viewfunc(request):
....
@transaction.autocommit(using="my_other_database")
def viewfunc2(request):
....
@transaction.autocommit(using="my_other_database")
def viewfunc2(request):
....
Within ``viewfunc()``, transactions will be committed as soon as you call
``model.save()``, ``model.delete()``, or any other function that writes to the
database. ``viewfunc2()`` will have this same behavior, but for the
``"my_other_database"`` connection.
Within ``viewfunc()``, transactions will be committed as soon as you call
``model.save()``, ``model.delete()``, or any other function that writes to
the database. ``viewfunc2()`` will have this same behavior, but for the
``"my_other_database"`` connection.
``django.db.transaction.commit_on_success``
-------------------------------------------
.. function:: commit_on_success
Use the ``commit_on_success`` decorator to use a single transaction for
all the work done in a function::
Use the ``commit_on_success`` decorator to use a single transaction for all
the work done in a function::
from django.db import transaction
from django.db import transaction
@transaction.commit_on_success
def viewfunc(request):
....
@transaction.commit_on_success
def viewfunc(request):
....
@transaction.commit_on_success(using="my_other_database")
def viewfunc2(request):
....
@transaction.commit_on_success(using="my_other_database")
def viewfunc2(request):
....
If the function returns successfully, then Django will commit all work done
within the function at that point. If the function raises an exception, though,
Django will roll back the transaction.
If the function returns successfully, then Django will commit all work done
within the function at that point. If the function raises an exception,
though, Django will roll back the transaction.
``django.db.transaction.commit_manually``
-----------------------------------------
.. function:: commit_manually
Use the ``commit_manually`` decorator if you need full control over
transactions. It tells Django you'll be managing the transaction on your own.
Use the ``commit_manually`` decorator if you need full control over
transactions. It tells Django you'll be managing the transaction on your
own.
If your view changes data and doesn't ``commit()`` or ``rollback()``, Django
will raise a ``TransactionManagementError`` exception.
If your view changes data and doesn't ``commit()`` or ``rollback()``,
Django will raise a ``TransactionManagementError`` exception.
Manual transaction management looks like this::
Manual transaction management looks like this::
from django.db import transaction
from django.db import transaction
@transaction.commit_manually
def viewfunc(request):
...
# You can commit/rollback however and whenever you want
transaction.commit()
...
# But you've got to remember to do it yourself!
try:
@transaction.commit_manually
def viewfunc(request):
...
except:
transaction.rollback()
else:
# You can commit/rollback however and whenever you want
transaction.commit()
...
@transaction.commit_manually(using="my_other_database")
def viewfunc2(request):
....
# But you've got to remember to do it yourself!
try:
...
except:
transaction.rollback()
else:
transaction.commit()
.. admonition:: An important note to users of earlier Django releases:
The database ``connection.commit()`` and ``connection.rollback()`` methods
(called ``db.commit()`` and ``db.rollback()`` in 0.91 and earlier) no
longer exist. They've been replaced by ``transaction.commit()`` and
``transaction.rollback()``.
@transaction.commit_manually(using="my_other_database")
def viewfunc2(request):
....
How to globally deactivate transaction management
=================================================

View File

@ -1,3 +1,5 @@
import sys
from django.db import connection, transaction, IntegrityError, DEFAULT_DB_ALIAS
from django.conf import settings
from django.test import TransactionTestCase, skipUnlessDBFeature
@ -5,6 +7,10 @@ from django.test import TransactionTestCase, skipUnlessDBFeature
from models import Reporter
if sys.version_info >= (2, 5):
from tests_25 import TransactionContextManagerTests
class TransactionTests(TransactionTestCase):
def create_a_reporter_then_fail(self, first, last):
a = Reporter(first_name=first, last_name=last)

View File

@ -0,0 +1,123 @@
from __future__ import with_statement
from django.db import connection, transaction, IntegrityError
from django.test import TransactionTestCase, skipUnlessDBFeature
from models import Reporter
class TransactionContextManagerTests(TransactionTestCase):
def create_reporter_and_fail(self):
Reporter.objects.create(first_name="Bob", last_name="Holtzman")
raise Exception
@skipUnlessDBFeature('supports_transactions')
def test_autocommit(self):
"""
The default behavior is to autocommit after each save() action.
"""
with self.assertRaises(Exception):
self.create_reporter_and_fail()
# The object created before the exception still exists
self.assertEqual(Reporter.objects.count(), 1)
@skipUnlessDBFeature('supports_transactions')
def test_autocommit_context_manager(self):
"""
The autocommit context manager works exactly the same as the default
behavior.
"""
with self.assertRaises(Exception):
with transaction.autocommit():
self.create_reporter_and_fail()
self.assertEqual(Reporter.objects.count(), 1)
@skipUnlessDBFeature('supports_transactions')
def test_autocommit_context_manager_with_using(self):
"""
The autocommit context manager also works with a using argument.
"""
with self.assertRaises(Exception):
with transaction.autocommit(using="default"):
self.create_reporter_and_fail()
self.assertEqual(Reporter.objects.count(), 1)
@skipUnlessDBFeature('supports_transactions')
def test_commit_on_success(self):
"""
With the commit_on_success context manager, the transaction is only
committed if the block doesn't throw an exception.
"""
with self.assertRaises(Exception):
with transaction.commit_on_success():
self.create_reporter_and_fail()
self.assertEqual(Reporter.objects.count(), 0)
@skipUnlessDBFeature('supports_transactions')
def test_commit_on_success_with_using(self):
"""
The commit_on_success context manager also works with a using argument.
"""
with self.assertRaises(Exception):
with transaction.commit_on_success(using="default"):
self.create_reporter_and_fail()
self.assertEqual(Reporter.objects.count(), 0)
@skipUnlessDBFeature('supports_transactions')
def test_commit_on_success_succeed(self):
"""
If there aren't any exceptions, the data will get saved.
"""
Reporter.objects.create(first_name="Alice", last_name="Smith")
with transaction.commit_on_success():
Reporter.objects.filter(first_name="Alice").delete()
self.assertQuerysetEqual(Reporter.objects.all(), [])
@skipUnlessDBFeature('supports_transactions')
def test_manually_managed(self):
"""
You can manually manage transactions if you really want to, but you
have to remember to commit/rollback.
"""
with transaction.commit_manually():
Reporter.objects.create(first_name="Libby", last_name="Holtzman")
transaction.commit()
self.assertEqual(Reporter.objects.count(), 1)
@skipUnlessDBFeature('supports_transactions')
def test_manually_managed_mistake(self):
"""
If you forget, you'll get bad errors.
"""
with self.assertRaises(transaction.TransactionManagementError):
with transaction.commit_manually():
Reporter.objects.create(first_name="Scott", last_name="Browning")
@skipUnlessDBFeature('supports_transactions')
def test_manually_managed_with_using(self):
"""
The commit_manually function also works with a using argument.
"""
with self.assertRaises(transaction.TransactionManagementError):
with transaction.commit_manually(using="default"):
Reporter.objects.create(first_name="Walter", last_name="Cronkite")
@skipUnlessDBFeature('requires_rollback_on_dirty_transaction')
def test_bad_sql(self):
"""
Regression for #11900: If a block wrapped by commit_on_success
writes a transaction that can't be committed, that transaction should
be rolled back. The bug is only visible using the psycopg2 backend,
though the fix is generally a good idea.
"""
with self.assertRaises(IntegrityError):
with transaction.commit_on_success():
cursor = connection.cursor()
cursor.execute("INSERT INTO transactions_reporter (first_name, last_name) VALUES ('Douglas', 'Adams');")
transaction.set_dirty()
transaction.rollback()