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:
parent
cfbba28c39
commit
27db9378cf
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -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
|
||||
=================================================
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
Loading…
Reference in New Issue