Commit 27db9378 authored by Alex Gaynor's avatar Alex Gaynor
Browse files

Fixed #10771 -- added support for using the transaction management functions...

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
parent cfbba28c
Loading
Loading
Loading
Loading
+84 −60
Original line number Diff line number Diff line
@@ -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,31 +259,79 @@ 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.
    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 inner_autocommit(func, db=None):
        def _autocommit(*args, **kw):
    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:
                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)
                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):
    """
@@ -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
    def entering(using):
        enter_transaction_management(using=using)
        managed(True, using=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=db):
            if is_dirty(using=using):
                try:
                            commit(using=db)
                    commit(using=using)
                except:
                            rollback(using=db)
                    rollback(using=using)
                    raise
                return res
            finally:
                leave_transaction_management(using=db)
        return wraps(func)(_commit_on_success)

    # 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)
    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)
+13 −0
Original line number Diff line number Diff line
@@ -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
~~~~~~~~~~~~~~~

+103 −70
Original line number Diff line number Diff line
@@ -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,29 +50,70 @@ 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
===========================================

.. versionchanged:: 1.3
   Transaction management context managers are new in Django 1.3.

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.
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::

@@ -87,15 +128,14 @@ Example::
            ....

    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
    ``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

@@ -108,17 +148,17 @@ all the work done in a function::
            ....

    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.
    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.
    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::

@@ -143,13 +183,6 @@ Manual transaction management looks like this::
        def viewfunc2(request):
            ....

.. 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()``.

How to globally deactivate transaction management
=================================================

+6 −0
Original line number Diff line number Diff line
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)
+123 −0
Original line number Diff line number Diff line
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()