magic-removal: added transaction support to Django! see transactions.txt (in magic-removal) for the details.

git-svn-id: http://code.djangoproject.com/svn/django/branches/magic-removal@2457 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jacob Kaplan-Moss 2006-03-01 17:08:33 +00:00
parent 7901c93328
commit 2a65b00381
20 changed files with 551 additions and 45 deletions

View File

@ -1,6 +1,6 @@
"Daily cleanup file"
from django.db import backend, connection
from django.db import backend, connection, transaction
DOCUMENTATION_DIRECTORY = '/home/html/documentation/'
@ -11,7 +11,7 @@ def clean_up():
(backend.quote_name('core_sessions'), backend.quote_name('expire_date')))
cursor.execute("DELETE FROM %s WHERE %s < NOW() - INTERVAL '1 week'" % \
(backend.quote_name('registration_challenges'), backend.quote_name('request_date')))
connection.commit()
transaction.commit_unless_managed()
if __name__ == "__main__":
clean_up()

View File

@ -194,6 +194,10 @@ TIME_FORMAT = 'P'
# http://psyco.sourceforge.net/
ENABLE_PSYCO = False
# Do you want to manage transactions manually?
# Hint: you really don't!
TRANSACTIONS_MANAGED = False
##############
# MIDDLEWARE #
##############

View File

@ -1,7 +1,7 @@
"Database cache backend."
from django.core.cache.backends.base import BaseCache
from django.db import connection
from django.db import connection, transaction
import base64, time
from datetime import datetime
try:
@ -33,7 +33,7 @@ class CacheClass(BaseCache):
now = datetime.now()
if row[2] < now:
cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % self._table, [key])
connection.commit()
transaction.commit_unless_managed()
return default
return pickle.loads(base64.decodestring(row[1]))
@ -58,12 +58,12 @@ class CacheClass(BaseCache):
# To be threadsafe, updates/inserts are allowed to fail silently
pass
else:
connection.commit()
transaction.commit_unless_managed()
def delete(self, key):
cursor = connection.cursor()
cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % self._table, [key])
connection.commit()
transaction.commit_unless_managed()
def has_key(self, key):
cursor = connection.cursor()

View File

@ -218,7 +218,7 @@ get_sql_create.args = APP_ARGS
def get_sql_delete(app):
"Returns a list of the DROP TABLE SQL statements for the given app."
from django.db import backend, connection, models
from django.db import backend, connection, models, transaction
try:
cursor = connection.cursor()
@ -233,7 +233,7 @@ def get_sql_delete(app):
cursor.execute("SELECT 1 FROM %s LIMIT 1" % backend.quote_name('django_admin_log'))
except:
# The table doesn't exist, so it doesn't need to be dropped.
connection.rollback()
transaction.rollback_unless_managed()
admin_log_exists = False
else:
admin_log_exists = True
@ -252,7 +252,7 @@ def get_sql_delete(app):
cursor.execute("SELECT 1 FROM %s LIMIT 1" % backend.quote_name(klass._meta.db_table))
except:
# The table doesn't exist, so it doesn't need to be dropped.
connection.rollback()
transaction.rollback_unless_managed()
else:
opts = klass._meta
for f in opts.fields:
@ -268,7 +268,7 @@ def get_sql_delete(app):
cursor.execute("SELECT 1 FROM %s LIMIT 1" % backend.quote_name(klass._meta.db_table))
except:
# The table doesn't exist, so it doesn't need to be dropped.
connection.rollback()
transaction.rollback_unless_managed()
else:
output.append("DROP TABLE %s;" % backend.quote_name(klass._meta.db_table))
if backend.supports_constraints and references_to_delete.has_key(klass):
@ -290,7 +290,7 @@ def get_sql_delete(app):
if cursor is not None:
cursor.execute("SELECT 1 FROM %s LIMIT 1" % backend.quote_name(f.m2m_db_table()))
except:
connection.rollback()
transaction.rollback_unless_managed()
else:
output.append("DROP TABLE %s;" % backend.quote_name(f.m2m_db_table()))
@ -403,7 +403,7 @@ get_sql_all.args = APP_ARGS
# TODO: Check for model validation errors before executing SQL
def syncdb():
"Creates the database tables for all apps in INSTALLED_APPS whose tables haven't already been created."
from django.db import backend, connection, models, get_creation_module, get_introspection_module
from django.db import backend, connection, transaction, models, get_creation_module, get_introspection_module
introspection_module = get_introspection_module()
data_types = get_creation_module().DATA_TYPES
@ -470,7 +470,7 @@ def syncdb():
backend.quote_name(r_col), backend.quote_name(table), backend.quote_name(col))
cursor.execute(sql)
connection.commit()
transaction.commit_unless_managed()
syncdb.args = ''
def get_admin_index(app):
@ -499,7 +499,7 @@ get_admin_index.args = APP_ARGS
def install(app):
"Executes the equivalent of 'get_sql_all' in the current database."
from django.db import connection
from django.db import connection, transaction
from cStringIO import StringIO
app_name = app.__name__[app.__name__.rindex('.')+1:]
app_label = app_name.split('.')[-1]
@ -526,15 +526,15 @@ def install(app):
Hint: Look at the output of 'django-admin.py sqlall %s'. That's the SQL this command wasn't able to run.
The full error: %s\n""" % \
(app_name, app_label, e))
connection.rollback()
transaction.rollback_unless_managed()
sys.exit(1)
connection.commit()
transaction.commit_unless_managed()
install.help_doc = "Executes ``sqlall`` for the given app(s) in the current database."
install.args = APP_ARGS
def reset(app):
"Executes the equivalent of 'get_sql_reset' in the current database."
from django.db import connection
from django.db import connection, transaction
from cStringIO import StringIO
app_name = app.__name__[app.__name__.rindex('.')+1:]
app_label = app_name.split('.')[-1]
@ -568,9 +568,9 @@ Type 'yes' to continue, or 'no' to cancel: """)
Hint: Look at the output of 'django-admin.py sqlreset %s'. That's the SQL this command wasn't able to run.
The full error: %s\n""" % \
(app_name, app_label, e))
connection.rollback()
transaction.rollback_unless_managed()
sys.exit(1)
connection.commit()
transaction.commit_unless_managed()
else:
print "Reset cancelled."
reset.help_doc = "Executes ``sqlreset`` for the given app(s) in the current database."
@ -1035,7 +1035,7 @@ runserver.args = '[optional port number, or ipaddr:port]'
def createcachetable(tablename):
"Creates the table needed to use the SQL cache backend"
from django.db import backend, get_creation_module, models
from django.db import backend, connection, transaction, get_creation_module, models
data_types = get_creation_module().DATA_TYPES
fields = (
# "key" is a reserved word in MySQL, so use "cache_key" instead.
@ -1066,7 +1066,7 @@ def createcachetable(tablename):
curs.execute("\n".join(full_statement))
for statement in index_output:
curs.execute(statement)
connection.commit()
transaction.commit_unless_managed()
createcachetable.args = "[tablename]"
def run_shell(use_plain=False):

View File

@ -38,4 +38,7 @@ dispatcher.connect(reset_queries, signal=signals.request_started)
# Register an event that rolls back the connection
# when a Django request has an exception.
dispatcher.connect(lambda: connection.rollback(), signal=signals.got_request_exception)
def _rollback_on_exception():
from django.db import transaction
transaction.rollback_unless_managed()
dispatcher.connect(_rollback_on_exception, signal=signals.got_request_exception)

View File

@ -64,10 +64,10 @@ class DatabaseWrapper:
return base.CursorDebugWrapper(cursor, self)
return cursor
def commit(self):
def _commit(self):
return self.connection.commit()
def rollback(self):
def _rollback(self):
if self.connection:
return self.connection.rollback()

View File

@ -17,8 +17,8 @@ class DatabaseError(Exception):
class DatabaseWrapper:
cursor = complain
commit = complain
rollback = complain
_commit = complain
_rollback = complain
def close(self):
pass # close()

View File

@ -71,10 +71,10 @@ class DatabaseWrapper:
return util.CursorDebugWrapper(MysqlDebugWrapper(cursor), self)
return cursor
def commit(self):
def _commit(self):
self.connection.commit()
def rollback(self):
def _rollback(self):
if self.connection:
try:
self.connection.rollback()

View File

@ -37,10 +37,10 @@ class DatabaseWrapper:
return util.CursorDebugWrapper(cursor, self)
return cursor
def commit(self):
def _commit(self):
return self.connection.commit()
def rollback(self):
def _rollback(self):
if self.connection:
return self.connection.rollback()

View File

@ -39,10 +39,10 @@ class DatabaseWrapper:
else:
return cursor
def commit(self):
def _commit(self):
self.connection.commit()
def rollback(self):
def _rollback(self):
if self.connection:
self.connection.rollback()
@ -67,7 +67,6 @@ class SQLiteCursorWrapper(Database.Cursor):
return Database.Cursor.executemany(self, query, params)
def convert_query(self, query, num_params):
# XXX this seems too simple to be correct... is this right?
return query % tuple("?" * num_params)
supports_constraints = False

View File

@ -5,7 +5,7 @@ from django.db.models.fields.related import OneToOne, ManyToOne
from django.db.models.related import RelatedObject
from django.db.models.query import orderlist2sql, delete_objects
from django.db.models.options import Options, AdminOptions
from django.db import connection, backend
from django.db import connection, backend, transaction
from django.db.models import signals
from django.db.models.loading import register_models
from django.dispatch import dispatcher
@ -184,7 +184,7 @@ class Model(object):
','.join(placeholders)), db_values)
if self._meta.has_auto_field and not pk_set:
setattr(self, self._meta.pk.attname, backend.get_last_insert_id(cursor, self._meta.db_table, self._meta.pk.column))
connection.commit()
transaction.commit_unless_managed()
# Run any post-save hooks.
dispatcher.send(signal=signals.post_save, sender=self.__class__, instance=self)
@ -340,7 +340,7 @@ class Model(object):
backend.quote_name(rel_field.m2m_column_name()),
backend.quote_name(rel_field.m2m_reverse_name()))
cursor.executemany(sql, [(this_id, i) for i in id_list])
connection.commit()
transaction.commit_unless_managed()
############################################
# HELPER FUNCTIONS (CURRIED MODEL METHODS) #
@ -357,7 +357,7 @@ def method_set_order(ordered_obj, self, id_list):
backend.quote_name(ordered_obj.pk.column))
rel_val = getattr(self, ordered_obj.order_with_respect_to.rel.field_name)
cursor.executemany(sql, [(i, rel_val, j) for i, j in enumerate(id_list)])
connection.commit()
transaction.commit_unless_managed()
def method_get_order(ordered_obj, self):
cursor = connection.cursor()

View File

@ -1,4 +1,4 @@
from django.db import backend, connection
from django.db import backend, connection, transaction
from django.db.models import signals
from django.db.models.fields import AutoField, Field, IntegerField
from django.db.models.related import RelatedObject
@ -231,7 +231,7 @@ def _add_m2m_items(rel_manager_inst, managerclass, rel_model, join_table, source
cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \
(join_table, source_col_name, target_col_name),
[source_pk_val, obj_id])
connection.commit()
transaction.commit_unless_managed()
def _remove_m2m_items(rel_model, join_table, source_col_name,
target_col_name, source_pk_val, *objs):
@ -255,7 +255,7 @@ def _remove_m2m_items(rel_model, join_table, source_col_name,
cursor.execute("DELETE FROM %s WHERE %s = %%s AND %s = %%s" % \
(join_table, source_col_name, target_col_name),
[source_pk_val, obj._get_pk_val()])
connection.commit()
transaction.commit_unless_managed()
def _clear_m2m_items(join_table, source_col_name, source_pk_val):
# Utility function used by the ManyRelatedObjectsDescriptors
@ -268,7 +268,7 @@ def _clear_m2m_items(join_table, source_col_name, source_pk_val):
cursor.execute("DELETE FROM %s WHERE %s = %%s" % \
(join_table, source_col_name),
[source_pk_val])
connection.commit()
transaction.commit_unless_managed()
class ManyRelatedObjectsDescriptor(object):
# This class provides the functionality that makes the related-object

View File

@ -1,4 +1,4 @@
from django.db import backend, connection
from django.db import backend, connection, transaction
from django.db.models.fields import DateField, FieldDoesNotExist
from django.db.models import signals
from django.dispatch import dispatcher
@ -846,4 +846,4 @@ def delete_objects(seen_objs):
setattr(instance, cls._meta.pk.attname, None)
dispatcher.send(signal=signals.post_delete, sender=cls, instance=instance)
connection.commit()
transaction.commit_unless_managed()

225
django/db/transaction.py Normal file
View File

@ -0,0 +1,225 @@
"""
This module implements a transaction manager that can be used to define
transaction handling in a request or view function. It is used by transaction
control middleware and decorators.
The transaction manager can be in managed or in auto state. Auto state means the
system is using a commit-on-save strategy (actually it's more like
commit-on-change). As soon as the .save() or .delete() (or related) methods are
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 thread
from django.db import connection
from django.conf import settings
class TransactionManagementError(Exception):
"""
This is the exception that is thrown when
something bad happens with transaction management.
"""
pass
# The state is a dictionary of lists. The key to the dict is the current
# thread and the list is handled as a stack of values.
state = {}
# The dirty flag is set by *_unless_managed functions to denote that the
# code under transaction management has changed things to require a
# database commit.
dirty = {}
def enter_transaction_management():
"""
Enters transaction management for a running thread. It must be balanced with
the appropriate leave_transaction_management call, since the actual state is
managed as a stack.
The state and dirty flag are carried over from the surrounding block or
from the settings, if there is no surrounding block (dirty is allways false
when no current block is running).
"""
thread_ident = thread.get_ident()
if state.has_key(thread_ident) and state[thread_ident]:
state[thread_ident].append(state[thread_ident][-1])
else:
state[thread_ident] = []
state[thread_ident].append(settings.TRANSACTIONS_MANAGED)
if not dirty.has_key(thread_ident):
dirty[thread_ident] = False
def leave_transaction_management():
"""
Leaves transaction management for a running thread. A dirty flag is carried
over to the surrounding block, as a commit will commit all changes, even
those from outside (commits are on connection level).
"""
thread_ident = thread.get_ident()
if state.has_key(thread_ident) and state[thread_ident]:
del state[thread_ident][-1]
else:
raise TransactionManagementError("This code isn't under transaction management")
if dirty.get(thread_ident, False):
# I fixed it for you this time, but don't do it again!
rollback()
raise TransactionManagementError("Transaction managed block ended with pending COMMIT/ROLLBACK")
dirty[thread_ident] = False
def is_dirty():
"""
Checks if the current transaction requires a commit for changes to happen.
"""
return dirty.get(thread.get_ident(), False)
def set_dirty():
"""
Sets a dirty flag for the current thread and code streak. This can be used
to decide in a managed block of code to decide whether there are open
changes waiting for commit.
"""
thread_ident = thread.get_ident()
if dirty.has_key(thread_ident):
dirty[thread_ident] = True
else:
raise TransactionManagementError("This code isn't under transaction management")
def set_clean():
"""
Resets a dirty flag for the current thread and code streak. This can be used
to decide in a managed block of code to decide whether there should happen a
commit or rollback.
"""
thread_ident = thread.get_ident()
if dirty.has_key(thread_ident):
dirty[thread_ident] = False
else:
raise TransactionManagementError("This code isn't under transaction management")
def is_managed():
"""
Checks whether the transaction manager is in manual or in auto state.
"""
thread_ident = thread.get_ident()
if state.has_key(thread_ident):
if state[thread_ident]:
return state[thread_ident][-1]
return settings.TRANSACTIONS_MANAGED
def managed(flag=True):
"""
Puts the transaction manager into a manual state - managed transactions have
to be committed explicitely by the user. If you switch off transaction
management and there is a pending commit/rollback, the data will be
commited.
"""
thread_ident = thread.get_ident()
top = state.get(thread_ident, None)
if top:
top[-1] = flag
if not flag and is_dirty():
connection._commit()
set_clean()
else:
raise TransactionManagementError("This code isn't under transaction management")
def commit_unless_managed():
"""
Commits changes if the system is not in managed transaction mode.
"""
if not is_managed():
connection._commit()
else:
set_dirty()
def rollback_unless_managed():
"""
Rolls back changes if the system is not in managed transaction mode.
"""
if not is_managed():
connection._rollback()
else:
set_dirty()
def commit():
"""
Does the commit itself and resets the dirty flag.
"""
connection._commit()
set_clean()
def rollback():
"""
This function does the rollback itself and resets the dirty flag.
"""
connection._rollback()
set_clean()
##############
# DECORATORS #
##############
def autocommit(func):
"""
Decorator that activates commit on save. This is Django's default behavour;
this decorator is useful if you globally activated transaction management in
your settings file and want the default behaviour in some view functions.
"""
def _autocommit(*args, **kw):
try:
enter_transaction_management()
managed(False)
return func(*args, **kw)
finally:
leave_transaction_management()
return _autocommit
def commit_on_success(func):
"""
This decorator activates commit on response. This way if the viewfunction
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
control in web apps.
"""
def _commit_on_success(*args, **kw):
try:
enter_transaction_management()
managed(True)
try:
res = func(*args, **kw)
except Exception, e:
if is_dirty():
rollback()
raise e
else:
if is_dirty():
commit()
return res
finally:
leave_transaction_management()
return _commit_on_success
def commit_manually(func):
"""
Decorator that activates manual transaction control. It just disables
automatic transaction control and doesn't do any commit/rollback of it's own
- it's up to the user to call the commit and rollback functions themselves.
"""
def _commit_manually(*args, **kw):
try:
enter_transaction_management()
managed(True)
return func(*args, **kw)
finally:
leave_transaction_management()
return _commit_manually

View File

@ -0,0 +1,29 @@
from django.conf import settings
from django.db import transaction
class TransactionMiddleware:
"""
Transaction middleware. If this is enabled, each view function will be run
with commit_on_response activated - that way a save() doesn't do a direct
commit, the commit is done when a successfull response is created. If an
exception happens, the database is rolled back.
"""
def process_request(self, request):
"""Enters transaction management"""
transaction.enter_transaction_management()
transaction.managed(True)
def process_exception(self, request, exception):
"""Rolls back the database and leaves transaction management"""
if transaction.is_dirty():
transaction.rollback()
transaction.leave_transaction_management()
def process_response(self, request, response):
"""Commits and leaves transaction management."""
if transaction.is_managed():
if transaction.is_dirty():
transaction.commit()
transaction.leave_transaction_management()
return response

View File

@ -102,6 +102,23 @@ Enables session support. See the `session documentation`_.
.. _`session documentation`: http://www.djangoproject.com/documentation/sessions/
django.middleware.transaction.TransactionMiddleware
---------------------------------------------------
Binds commit and rollback to the request/response phase. If a view function runs
successfully, a commit is done. If it fails with an exception, a rollback is
done.
The order of this middleware in the stack is important: middleware modules
running outside of it run with commit-on-save - the default Django behavior.
Middleware modules running inside it (coming later in the stack) will be under
the same transaction control as the view functions.
See the `transaction management documentation`_.
.. _`transaction management documentation`: http://www.djangoproject.com/documentation/transaction/
Writing your own middleware
===========================

138
docs/transactions.txt Normal file
View File

@ -0,0 +1,138 @@
==============================
Managing database transactions
==============================
Django gives you a few ways to control how database transactions are managed.
Django's default transaction behavior
=====================================
The default behavior of Django is to commit on special model functions. If you
call ``model.save()`` or ``model.delete()``, that change will be committed immediately.
This is much like the auto-commit setting for most databases: as soon as you
perform an action that needs to write to the database, Django produces the
insert/update/delete statements and then does the commit. There is no implicit
rollback in Django.
Tying transactions to HTTP requests
===================================
A useful way to handle transactions is to tie them to the request and response
phases.
When a request starts, you start a transaction. If the response is produced
without problems, any transactions are committed. If the view function produces
and exception, a rollback happens. This is one of the more intuitive ways to
handle transactions. To activate this feature, just add the
``TransactionMiddleware`` middleware to your stack::
MIDDLEWARE_CLASSES = (
"django.middleware.common.CommonMiddleware",
"django.middleware.sessions.SessionMiddleware",
"django.middleware.cache.CacheMiddleware",
"django.middleware.transaction.TransactionMiddleware",
)
The order is quite important: the transaction middleware will be relevant not
only for the view functions called, but for all middleware modules that come
after it. So if you use the session middleware after the transaction middleware,
session creation will be part of the transaction.
The cache middleware isn't affected as it uses it's own database cursor (that is
mapped to it's own database connection internally) and only the database based
cache is affected.
Controlling transaction management in views
===========================================
For many people, implicit request-based transactions will work wonderfully.
However, if you need to control the way that transactions are managed,
there are a set of decorators that you can apply to a function to change
the way transactions are handled.
.. note::
Although the examples below use view functions as examples, these
decorators can be applied to non-view functions as well.
``autocommit``
--------------
You can use the ``autocommit`` decorator to switch a view function to the
default commit behavior of Django, regardless of the global setting. Just use
the decorator like this::
from django.db.transaction import autocommit
@transaction.autocommit
def viewfunc(request):
....
Within ``viewfunc`` transactions will be comitted as soon as you call
``model.save()``, ``model.delete()``, or any similar function that writes to the
database.
``commit_on_success``
---------------------
You can use the ``commit_on_success`` decorator to use a single transaction for
all the work done in a function::
from django.db.transaction import commit_on_success
@commit_on_success
def viewfunc(request):
....
If the function returns successfully then all work done will be committed. If an
exception is raised beyond the function, however, the transaction will be rolled
back.
``commit_manually``
-------------------
Sometimes you need full control over your transactions. In that case, you can use the
``commit_manually`` decorator which will make you run your own transaction management.
If you don't commit or rollback and did change data (so that the current transaction
is marked as dirty), you will get a ``TransactionManagementError`` exception saying so.
Manual transaction management looks like::
from django.db import transaction
@transaction.commit_manually
def viewfunc(request):
...
transaction.commit()
...
try:
...
except:
transaction.rollback()
else:
transaction.commit()
..admonition:: An important note to users of earlier django releases:
The database ``connection.commit`` and ``connection.rollback`` functions
(also called ``db.commit`` and ``db.rollback`` in 0.91 and earlier), no
longer exist and have been replaced by the ``transaction.commit`` and
``transaction.rollback`` commands.
How to globally deactivate transaction management
=================================================
Control freaks can totally disable all transaction management by setting
``DISABLE_TRANSACTION_MANAGEMENT`` to ``True`` in your settings file.
If you do this, there will be no management whatsoever. The middleware will no
longer implicitly commit transactions, and you'll need to roll management
yourself. This even will require you to commit changes done by middleware
somewhere else.
Thus, this is best used in situations where you want to run your own transaction
controlling middleware or do something really strange. In almost all situations
you'll be better off using the default behavior or the transaction middleware
and only modify selected functions as needed.

View File

@ -0,0 +1,91 @@
"""
XXX. Transactions
Django handles transactions in three different ways. The default is to commit
each transaction upon a write, but you can decorate a function to get
commit-on-sucess behavior, or else you can manage the transaction manually.
"""
from django.db import models
class Reporter(models.Model):
first_name = models.CharField(maxlength=30)
last_name = models.CharField(maxlength=30)
email = models.EmailField()
def __repr__(self):
return "%s %s" % (self.first_name, self.last_name)
API_TESTS = """
>>> from django.db import connection, transaction
# the default behavior is to autocommit after each save() action
>>> def create_a_reporter_then_fail(first, last):
... a = Reporter(first_name=first, last_name=last)
... a.save()
... raise Exception("I meant to do that")
...
>>> create_a_reporter_then_fail("Alice", "Smith")
Traceback (most recent call last):
...
Exception: I meant to do that
# The object created before the exception still exists
>>> Reporter.objects.all()
[Alice Smith]
# the autocommit decorator works exactly the same as the default behavior
>>> autocomitted_create_then_fail = transaction.autocommit(create_a_reporter_then_fail)
>>> autocomitted_create_then_fail("Ben", "Jones")
Traceback (most recent call last):
...
Exception: I meant to do that
# Same behavior as before
>>> Reporter.objects.all()
[Alice Smith, Ben Jones]
# With the commit_on_success decorator, the transaction is only comitted if the
# function doesn't throw an exception
>>> committed_on_success = transaction.commit_on_success(create_a_reporter_then_fail)
>>> committed_on_success("Carol", "Doe")
Traceback (most recent call last):
...
Exception: I meant to do that
# This time the object never got saved
>>> Reporter.objects.all()
[Alice Smith, Ben Jones]
# If there aren't any exceptions, the data will get saved
>>> def remove_a_reporter():
... r = Reporter.objects.get(first_name="Alice")
... r.delete()
...
>>> remove_comitted_on_success = transaction.commit_on_success(remove_a_reporter)
>>> remove_comitted_on_success()
>>> Reporter.objects.all()
[Ben Jones]
# You can manually manage transactions if you really want to, but you
# have to remember to commit/rollback
>>> def manually_managed():
... r = Reporter(first_name="Carol", last_name="Doe")
... r.save()
... transaction.commit()
>>> manually_managed = transaction.commit_manually(manually_managed)
>>> manually_managed()
>>> Reporter.objects.all()
[Ben Jones, Carol Doe]
# If you forget, you'll get bad errors
>>> def manually_managed_mistake():
... r = Reporter(first_name="David", last_name="Davidson")
... r.save()
... # oops, I forgot to commit/rollback!
>>> manually_managed_mistake = transaction.commit_manually(manually_managed_mistake)
>>> manually_managed_mistake()
Traceback (most recent call last):
...
TransactionManagementError: Transaction managed block ended with pending COMMIT/ROLLBACK
"""

View File

@ -39,13 +39,13 @@ class DjangoDoctestRunner(doctest.DocTestRunner):
"Code: %r\nLine: %s\nExpected: %r\nGot: %r" % (example.source.strip(), example.lineno, example.want, got))
def report_unexpected_exception(self, out, test, example, exc_info):
from django.db import connection
from django.db import transaction
tb = ''.join(traceback.format_exception(*exc_info)[1:])
log_error(test.name, "API test raised an exception",
"Code: %r\nLine: %s\nException: %s" % (example.source.strip(), example.lineno, tb))
# Rollback, in case of database errors. Otherwise they'd have
# side effects on other tests.
connection.rollback()
transaction.rollback_unless_managed()
normalize_long_ints = lambda s: re.sub(r'(?<![\w])(\d+)L(?![\w])', '\\1', s)