Deprecated TransactionMiddleware and TRANSACTIONS_MANAGED.

Replaced them with per-database options, for proper multi-db support.

Also toned down the recommendation to tie transactions to HTTP requests.
Thanks Jeremy for sharing his experience.
This commit is contained in:
Aymeric Augustin 2013-03-06 11:12:24 +01:00
parent f7245b83bb
commit ac37ed21b3
12 changed files with 217 additions and 51 deletions

View File

@ -6,10 +6,10 @@ import types
from django import http from django import http
from django.conf import settings from django.conf import settings
from django.core import exceptions
from django.core import urlresolvers from django.core import urlresolvers
from django.core import signals from django.core import signals
from django.core.exceptions import MiddlewareNotUsed, PermissionDenied from django.core.exceptions import MiddlewareNotUsed, PermissionDenied
from django.db import connections, transaction
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.module_loading import import_by_path from django.utils.module_loading import import_by_path
from django.utils import six from django.utils import six
@ -65,6 +65,13 @@ class BaseHandler(object):
# as a flag for initialization being complete. # as a flag for initialization being complete.
self._request_middleware = request_middleware self._request_middleware = request_middleware
def make_view_atomic(self, view):
if getattr(view, 'transactions_per_request', True):
for db in connections.all():
if db.settings_dict['ATOMIC_REQUESTS']:
view = transaction.atomic(using=db.alias)(view)
return view
def get_response(self, request): def get_response(self, request):
"Returns an HttpResponse object for the given HttpRequest" "Returns an HttpResponse object for the given HttpRequest"
try: try:
@ -101,8 +108,9 @@ class BaseHandler(object):
break break
if response is None: if response is None:
wrapped_callback = self.make_view_atomic(callback)
try: try:
response = callback(request, *callback_args, **callback_kwargs) response = wrapped_callback(request, *callback_args, **callback_kwargs)
except Exception as e: except Exception as e:
# If the view raised an exception, run it through exception # If the view raised an exception, run it through exception
# middleware, and if the exception middleware returns a # middleware, and if the exception middleware returns a

View File

@ -104,7 +104,7 @@ class BaseDatabaseWrapper(object):
conn_params = self.get_connection_params() conn_params = self.get_connection_params()
self.connection = self.get_new_connection(conn_params) self.connection = self.get_new_connection(conn_params)
self.init_connection_state() self.init_connection_state()
if not settings.TRANSACTIONS_MANAGED: if self.settings_dict['AUTOCOMMIT']:
self.set_autocommit() self.set_autocommit()
connection_created.send(sender=self.__class__, connection=self) connection_created.send(sender=self.__class__, connection=self)
@ -299,7 +299,7 @@ class BaseDatabaseWrapper(object):
if self.transaction_state: if self.transaction_state:
managed = self.transaction_state[-1] managed = self.transaction_state[-1]
else: else:
managed = settings.TRANSACTIONS_MANAGED managed = not self.settings_dict['AUTOCOMMIT']
if self._dirty: if self._dirty:
self.rollback() self.rollback()

View File

@ -2,6 +2,7 @@ from functools import wraps
import os import os
import pkgutil import pkgutil
from threading import local from threading import local
import warnings
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
@ -158,6 +159,13 @@ class ConnectionHandler(object):
except KeyError: except KeyError:
raise ConnectionDoesNotExist("The connection %s doesn't exist" % alias) raise ConnectionDoesNotExist("The connection %s doesn't exist" % alias)
conn.setdefault('ATOMIC_REQUESTS', False)
if settings.TRANSACTIONS_MANAGED:
warnings.warn(
"TRANSACTIONS_MANAGED is deprecated. Use AUTOCOMMIT instead.",
PendingDeprecationWarning, stacklevel=2)
conn.setdefault('AUTOCOMMIT', False)
conn.setdefault('AUTOCOMMIT', True)
conn.setdefault('ENGINE', 'django.db.backends.dummy') conn.setdefault('ENGINE', 'django.db.backends.dummy')
if conn['ENGINE'] == 'django.db.backends.' or not conn['ENGINE']: if conn['ENGINE'] == 'django.db.backends.' or not conn['ENGINE']:
conn['ENGINE'] = 'django.db.backends.dummy' conn['ENGINE'] = 'django.db.backends.dummy'

View File

@ -1,4 +1,7 @@
from django.db import transaction import warnings
from django.core.exceptions import MiddlewareNotUsed
from django.db import connection, transaction
class TransactionMiddleware(object): class TransactionMiddleware(object):
""" """
@ -7,6 +10,14 @@ class TransactionMiddleware(object):
commit, the commit is done when a successful response is created. If an commit, the commit is done when a successful response is created. If an
exception happens, the database is rolled back. exception happens, the database is rolled back.
""" """
def __init__(self):
warnings.warn(
"TransactionMiddleware is deprecated in favor of ATOMIC_REQUESTS.",
PendingDeprecationWarning, stacklevel=2)
if connection.settings_dict['ATOMIC_REQUESTS']:
raise MiddlewareNotUsed
def process_request(self, request): def process_request(self, request):
"""Enters transaction management""" """Enters transaction management"""
transaction.enter_transaction_management() transaction.enter_transaction_management()

View File

@ -329,9 +329,14 @@ these changes.
1.8 1.8
--- ---
* The decorators and context managers ``django.db.transaction.autocommit``, * The following transaction management APIs will be removed:
``commit_on_success`` and ``commit_manually`` will be removed. See
:ref:`transactions-upgrading-from-1.5`. - ``TransactionMiddleware``,
- the decorators and context managers ``autocommit``, ``commit_on_success``,
and ``commit_manually``,
- the ``TRANSACTIONS_MANAGED`` setting.
Upgrade paths are described in :ref:`transactions-upgrading-from-1.5`.
* The :ttag:`cycle` and :ttag:`firstof` template tags will auto-escape their * The :ttag:`cycle` and :ttag:`firstof` template tags will auto-escape their
arguments. In 1.6 and 1.7, this behavior is provided by the version of these arguments. In 1.6 and 1.7, this behavior is provided by the version of these

View File

@ -205,6 +205,10 @@ Transaction middleware
.. class:: TransactionMiddleware .. class:: TransactionMiddleware
.. versionchanged:: 1.6
``TransactionMiddleware`` is deprecated. The documentation of transactions
contains :ref:`upgrade instructions <transactions-upgrading-from-1.5>`.
Binds commit and rollback of the default database to the request/response Binds commit and rollback of the default database to the request/response
phase. If a view function runs successfully, a commit is done. If it fails with phase. If a view function runs successfully, a commit is done. If it fails with
an exception, a rollback is done. an exception, a rollback is done.

View File

@ -408,6 +408,30 @@ SQLite. This can be configured using the following::
For other database backends, or more complex SQLite configurations, other options For other database backends, or more complex SQLite configurations, other options
will be required. The following inner options are available. will be required. The following inner options are available.
.. setting:: DATABASE-ATOMIC_REQUESTS
ATOMIC_REQUESTS
~~~~~~~~~~~~~~~
.. versionadded:: 1.6
Default: ``False``
Set this to ``True`` to wrap each HTTP request in a transaction on this
database. See :ref:`tying-transactions-to-http-requests`.
.. setting:: DATABASE-AUTOCOMMIT
AUTOCOMMIT
~~~~~~~~~~
.. versionadded:: 1.6
Default: ``True``
Set this to ``False`` if you want to :ref:`disable Django's transaction
management <deactivate-transaction-management>` and implement your own.
.. setting:: DATABASE-ENGINE .. setting:: DATABASE-ENGINE
ENGINE ENGINE
@ -1807,6 +1831,12 @@ to ensure your processes are running in the correct environment.
TRANSACTIONS_MANAGED TRANSACTIONS_MANAGED
-------------------- --------------------
.. deprecated:: 1.6
This setting was deprecated because its name is very misleading. Use the
:setting:`AUTOCOMMIT <DATABASE-AUTOCOMMIT>` key in :setting:`DATABASES`
entries instead.
Default: ``False`` Default: ``False``
Set this to ``True`` if you want to :ref:`disable Django's transaction Set this to ``True`` if you want to :ref:`disable Django's transaction

View File

@ -262,9 +262,11 @@ Transaction management APIs
Transaction management was completely overhauled in Django 1.6, and the Transaction management was completely overhauled in Django 1.6, and the
current APIs are deprecated: current APIs are deprecated:
- :func:`django.db.transaction.autocommit` - ``django.middleware.transaction.TransactionMiddleware``
- :func:`django.db.transaction.commit_on_success` - ``django.db.transaction.autocommit``
- :func:`django.db.transaction.commit_manually` - ``django.db.transaction.commit_on_success``
- ``django.db.transaction.commit_manually``
- the ``TRANSACTIONS_MANAGED`` setting
The reasons for this change and the upgrade path are described in the The reasons for this change and the upgrade path are described in the
:ref:`transactions documentation <transactions-upgrading-from-1.5>`. :ref:`transactions documentation <transactions-upgrading-from-1.5>`.

View File

@ -26,45 +26,61 @@ immediately committed to the database. :ref:`See below for details
Previous version of Django featured :ref:`a more complicated default Previous version of Django featured :ref:`a more complicated default
behavior <transactions-upgrading-from-1.5>`. behavior <transactions-upgrading-from-1.5>`.
.. _tying-transactions-to-http-requests:
Tying transactions to HTTP requests Tying transactions to HTTP requests
----------------------------------- -----------------------------------
The recommended way to handle transactions in Web requests is to tie them to A common way to handle transactions on the web is to wrap each request in a
the request and response phases via Django's ``TransactionMiddleware``. transaction. Set :setting:`ATOMIC_REQUESTS <DATABASE-ATOMIC_REQUESTS>` to
``True`` in the configuration of each database for which you want to enable
this behavior.
It works like this. When a request starts, Django starts a transaction. If the It works like this. When a request starts, Django starts a transaction. If the
response is produced without problems, Django commits any pending transactions. response is produced without problems, Django commits the transaction. If the
If the view function produces an exception, Django rolls back any pending view function produces an exception, Django rolls back the transaction.
transactions. Middleware always runs outside of this transaction.
To activate this feature, just add the ``TransactionMiddleware`` middleware to You may perfom partial commits and rollbacks in your view code, typically with
your :setting:`MIDDLEWARE_CLASSES` setting:: the :func:`atomic` context manager. However, at the end of the view, either
all the changes will be committed, or none of them.
MIDDLEWARE_CLASSES = ( To disable this behavior for a specific view, you must set the
'django.middleware.cache.UpdateCacheMiddleware', ``transactions_per_request`` attribute of the view function itself to
'django.contrib.sessions.middleware.SessionMiddleware', ``False``, like this::
'django.middleware.common.CommonMiddleware',
'django.middleware.transaction.TransactionMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware',
)
The order is quite important. The transaction middleware applies not only to def my_view(request):
view functions, but also for all middleware modules that come after it. So if do_stuff()
you use the session middleware after the transaction middleware, session my_view.transactions_per_request = False
creation will be part of the transaction.
The various cache middlewares are an exception: ``CacheMiddleware``, .. warning::
:class:`~django.middleware.cache.UpdateCacheMiddleware`, and
:class:`~django.middleware.cache.FetchFromCacheMiddleware` are never affected.
Even when using database caching, Django's cache backend uses its own database
connection internally.
.. note:: While the simplicity of this transaction model is appealing, it also makes it
inefficient when traffic increases. Opening a transaction for every view has
some overhead. The impact on performance depends on the query patterns of your
application and on how well your database handles locking.
The ``TransactionMiddleware`` only affects the database aliased .. admonition:: Per-request transactions and streaming responses
as "default" within your :setting:`DATABASES` setting. If you are using
multiple databases and want transaction control over databases other than When a view returns a :class:`~django.http.StreamingHttpResponse`, reading
"default", you will need to write your own transaction middleware. the contents of the response will often execute code to generate the
content. Since the view has already returned, such code runs outside of
the transaction.
Generally speaking, it isn't advisable to write to the database while
generating a streaming response, since there's no sensible way to handle
errors after starting to send the response.
In practice, this feature simply wraps every view function in the :func:`atomic`
decorator described below.
Note that only the execution of your view in enclosed in the transactions.
Middleware run outside of the transaction, and so does the rendering of
template responses.
.. versionchanged:: 1.6
Django used to provide this feature via ``TransactionMiddleware``, which is
now deprecated.
Controlling transactions explicitly Controlling transactions explicitly
----------------------------------- -----------------------------------
@ -283,18 +299,20 @@ if autocommit is off. Django will also refuse to turn autocommit off when an
Deactivating transaction management Deactivating transaction management
----------------------------------- -----------------------------------
Control freaks can totally disable all transaction management by setting You can totally disable Django's transaction management for a given database
:setting:`TRANSACTIONS_MANAGED` to ``True`` in the Django settings file. If by setting :setting:`AUTOCOMMIT <DATABASE-AUTOCOMMIT>` to ``False`` in its
you do this, Django won't enable autocommit. You'll get the regular behavior configuration. If you do this, Django won't enable autocommit, and won't
of the underlying database library. perform any commits. You'll get the regular behavior of the underlying
database library.
This requires you to commit explicitly every transaction, even those started This requires you to commit explicitly every transaction, even those started
by Django or by third-party libraries. Thus, this is best used in situations by Django or by third-party libraries. Thus, this is best used in situations
where you want to run your own transaction-controlling middleware or do where you want to run your own transaction-controlling middleware or do
something really strange. something really strange.
In almost all situations, you'll be better off using the default behavior, or .. versionchanged:: 1.6
the transaction middleware, and only modify selected functions as needed. This used to be controlled by the ``TRANSACTIONS_MANAGED`` setting.
Database-specific notes Database-specific notes
======================= =======================
@ -459,6 +477,35 @@ atomicity of the outer block.
API changes API changes
----------- -----------
Transaction middleware
~~~~~~~~~~~~~~~~~~~~~~
In Django 1.6, ``TransactionMiddleware`` is deprecated and replaced
:setting:`ATOMIC_REQUESTS <DATABASE-ATOMIC_REQUESTS>`. While the general
behavior is the same, there are a few differences.
With the transaction middleware, it was still possible to switch to autocommit
or to commit explicitly in a view. Since :func:`atomic` guarantees atomicity,
this isn't allowed any longer.
To avoid wrapping a particular view in a transaction, instead of::
@transaction.autocommit
def my_view(request):
do_stuff()
you must now use this pattern::
def my_view(request):
do_stuff()
my_view.transactions_per_request = False
The transaction middleware applied not only to view functions, but also to
middleware modules that come after it. For instance, if you used the session
middleware after the transaction middleware, session creation was part of the
transaction. :setting:`ATOMIC_REQUESTS <DATABASE-ATOMIC_REQUESTS>` only
applies to the view itself.
Managing transactions Managing transactions
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
@ -508,6 +555,13 @@ you should now use::
finally: finally:
transaction.set_autocommit(autocommit=False) transaction.set_autocommit(autocommit=False)
Disabling transaction management
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Instead of setting ``TRANSACTIONS_MANAGED = True``, set the ``AUTOCOMMIT`` key
to ``False`` in the configuration of each database, as explained in :ref
:`deactivate-transaction-management`.
Backwards incompatibilities Backwards incompatibilities
--------------------------- ---------------------------

View File

@ -1,9 +1,8 @@
from django.core.handlers.wsgi import WSGIHandler from django.core.handlers.wsgi import WSGIHandler
from django.core.signals import request_started, request_finished from django.core.signals import request_started, request_finished
from django.db import close_old_connections from django.db import close_old_connections, connection
from django.test import RequestFactory, TestCase from django.test import RequestFactory, TestCase, TransactionTestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from django.utils import six
class HandlerTests(TestCase): class HandlerTests(TestCase):
@ -37,6 +36,31 @@ class HandlerTests(TestCase):
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
class TransactionsPerRequestTests(TransactionTestCase):
urls = 'handlers.urls'
def test_no_transaction(self):
response = self.client.get('/in_transaction/')
self.assertContains(response, 'False')
def test_auto_transaction(self):
old_atomic_requests = connection.settings_dict['ATOMIC_REQUESTS']
try:
connection.settings_dict['ATOMIC_REQUESTS'] = True
response = self.client.get('/in_transaction/')
finally:
connection.settings_dict['ATOMIC_REQUESTS'] = old_atomic_requests
self.assertContains(response, 'True')
def test_no_auto_transaction(self):
old_atomic_requests = connection.settings_dict['ATOMIC_REQUESTS']
try:
connection.settings_dict['ATOMIC_REQUESTS'] = True
response = self.client.get('/not_in_transaction/')
finally:
connection.settings_dict['ATOMIC_REQUESTS'] = old_atomic_requests
self.assertContains(response, 'False')
class SignalsTests(TestCase): class SignalsTests(TestCase):
urls = 'handlers.urls' urls = 'handlers.urls'

View File

@ -1,9 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from django.http import HttpResponse, StreamingHttpResponse
from . import views
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^regular/$', lambda request: HttpResponse(b"regular content")), url(r'^regular/$', views.regular),
url(r'^streaming/$', lambda request: StreamingHttpResponse([b"streaming", b" ", b"content"])), url(r'^streaming/$', views.streaming),
url(r'^in_transaction/$', views.in_transaction),
url(r'^not_in_transaction/$', views.not_in_transaction),
) )

17
tests/handlers/views.py Normal file
View File

@ -0,0 +1,17 @@
from __future__ import unicode_literals
from django.db import connection
from django.http import HttpResponse, StreamingHttpResponse
def regular(request):
return HttpResponse(b"regular content")
def streaming(request):
return StreamingHttpResponse([b"streaming", b" ", b"content"])
def in_transaction(request):
return HttpResponse(str(connection.in_atomic_block))
def not_in_transaction(request):
return HttpResponse(str(connection.in_atomic_block))
not_in_transaction.transactions_per_request = False