Implemented an 'atomic' decorator and context manager.
Currently it only works in autocommit mode. Based on @xact by Christophe Pettus.
This commit is contained in:
parent
4b31a6a9e6
commit
d7bc4fbc94
1
AUTHORS
1
AUTHORS
|
@ -434,6 +434,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Andreas Pelme <andreas@pelme.se>
|
||||
permonik@mesias.brnonet.cz
|
||||
peter@mymart.com
|
||||
Christophe Pettus <xof@thebuild.com>
|
||||
pgross@thoughtworks.com
|
||||
phaedo <http://phaedo.cx/>
|
||||
phil@produxion.net
|
||||
|
|
|
@ -50,6 +50,12 @@ class BaseDatabaseWrapper(object):
|
|||
# set somewhat aggressively, as the DBAPI doesn't make it easy to
|
||||
# deduce if the connection is in transaction or not.
|
||||
self._dirty = False
|
||||
# Tracks if the connection is in a transaction managed by 'atomic'
|
||||
self.in_atomic_block = False
|
||||
# List of savepoints created by 'atomic'
|
||||
self.savepoint_ids = []
|
||||
# Hack to provide compatibility with legacy transaction management
|
||||
self._atomic_forced_unmanaged = False
|
||||
|
||||
# Connection termination related attributes
|
||||
self.close_at = None
|
||||
|
@ -148,7 +154,7 @@ class BaseDatabaseWrapper(object):
|
|||
|
||||
def commit(self):
|
||||
"""
|
||||
Does the commit itself and resets the dirty flag.
|
||||
Commits a transaction and resets the dirty flag.
|
||||
"""
|
||||
self.validate_thread_sharing()
|
||||
self._commit()
|
||||
|
@ -156,7 +162,7 @@ class BaseDatabaseWrapper(object):
|
|||
|
||||
def rollback(self):
|
||||
"""
|
||||
Does the rollback itself and resets the dirty flag.
|
||||
Rolls back a transaction and resets the dirty flag.
|
||||
"""
|
||||
self.validate_thread_sharing()
|
||||
self._rollback()
|
||||
|
@ -447,6 +453,12 @@ class BaseDatabaseWrapper(object):
|
|||
if must_close:
|
||||
self.close()
|
||||
|
||||
def _start_transaction_under_autocommit(self):
|
||||
"""
|
||||
Only required when autocommits_when_autocommit_is_off = True.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class BaseDatabaseFeatures(object):
|
||||
allows_group_by_pk = False
|
||||
|
@ -549,6 +561,10 @@ class BaseDatabaseFeatures(object):
|
|||
# Support for the DISTINCT ON clause
|
||||
can_distinct_on_fields = False
|
||||
|
||||
# Does the backend decide to commit before SAVEPOINT statements
|
||||
# when autocommit is disabled? http://bugs.python.org/issue8145#msg109965
|
||||
autocommits_when_autocommit_is_off = False
|
||||
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
|
||||
|
@ -931,6 +947,9 @@ class BaseDatabaseOperations(object):
|
|||
return "BEGIN;"
|
||||
|
||||
def end_transaction_sql(self, success=True):
|
||||
"""
|
||||
Returns the SQL statement required to end a transaction.
|
||||
"""
|
||||
if not success:
|
||||
return "ROLLBACK;"
|
||||
return "COMMIT;"
|
||||
|
|
|
@ -99,6 +99,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
supports_mixed_date_datetime_comparisons = False
|
||||
has_bulk_insert = True
|
||||
can_combine_inserts_with_and_without_auto_increment_pk = False
|
||||
autocommits_when_autocommit_is_off = True
|
||||
|
||||
@cached_property
|
||||
def uses_savepoints(self):
|
||||
|
@ -360,10 +361,12 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
|||
BaseDatabaseWrapper.close(self)
|
||||
|
||||
def _savepoint_allowed(self):
|
||||
# When 'isolation_level' is None, Django doesn't provide a way to
|
||||
# create a transaction (yet) so savepoints can't be created. When it
|
||||
# isn't, sqlite3 commits before each savepoint -- it's a bug.
|
||||
return False
|
||||
# When 'isolation_level' is not None, sqlite3 commits before each
|
||||
# savepoint; it's a bug. When it is None, savepoints don't make sense
|
||||
# because autocommit is enabled. The only exception is inside atomic
|
||||
# blocks. To work around that bug, on SQLite, atomic starts a
|
||||
# transaction explicitly rather than simply disable autocommit.
|
||||
return self.in_atomic_block
|
||||
|
||||
def _set_autocommit(self, autocommit):
|
||||
if autocommit:
|
||||
|
@ -413,6 +416,14 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
|||
def is_usable(self):
|
||||
return True
|
||||
|
||||
def _start_transaction_under_autocommit(self):
|
||||
"""
|
||||
Start a transaction explicitly in autocommit mode.
|
||||
|
||||
Staying in autocommit mode works around a bug of sqlite3 that breaks
|
||||
savepoints when autocommit is disabled.
|
||||
"""
|
||||
self.cursor().execute("BEGIN")
|
||||
|
||||
FORMAT_QMARK_REGEX = re.compile(r'(?<!%)%s')
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import warnings
|
|||
|
||||
from functools import wraps
|
||||
|
||||
from django.db import connections, DEFAULT_DB_ALIAS
|
||||
from django.db import connections, DatabaseError, DEFAULT_DB_ALIAS
|
||||
|
||||
|
||||
class TransactionManagementError(Exception):
|
||||
|
@ -134,13 +134,13 @@ def rollback_unless_managed(using=None):
|
|||
|
||||
def commit(using=None):
|
||||
"""
|
||||
Does the commit itself and resets the dirty flag.
|
||||
Commits a transaction and resets the dirty flag.
|
||||
"""
|
||||
get_connection(using).commit()
|
||||
|
||||
def rollback(using=None):
|
||||
"""
|
||||
This function does the rollback itself and resets the dirty flag.
|
||||
Rolls back a transaction and resets the dirty flag.
|
||||
"""
|
||||
get_connection(using).rollback()
|
||||
|
||||
|
@ -166,9 +166,151 @@ def savepoint_commit(sid, using=None):
|
|||
"""
|
||||
get_connection(using).savepoint_commit(sid)
|
||||
|
||||
##############
|
||||
# DECORATORS #
|
||||
##############
|
||||
|
||||
#################################
|
||||
# Decorators / context managers #
|
||||
#################################
|
||||
|
||||
class Atomic(object):
|
||||
"""
|
||||
This class guarantees the atomic execution of a given block.
|
||||
|
||||
An instance can be used either as a decorator or as a context manager.
|
||||
|
||||
When it's used as a decorator, __call__ wraps the execution of the
|
||||
decorated function in the instance itself, used as a context manager.
|
||||
|
||||
When it's used as a context manager, __enter__ creates a transaction or a
|
||||
savepoint, depending on whether a transaction is already in progress, and
|
||||
__exit__ commits the transaction or releases the savepoint on normal exit,
|
||||
and rolls back the transaction or to the savepoint on exceptions.
|
||||
|
||||
A stack of savepoints identifiers is maintained as an attribute of the
|
||||
connection. None denotes a plain transaction.
|
||||
|
||||
This allows reentrancy even if the same AtomicWrapper is reused. For
|
||||
example, it's possible to define `oa = @atomic('other')` and use `@ao` or
|
||||
`with oa:` multiple times.
|
||||
|
||||
Since database connections are thread-local, this is thread-safe.
|
||||
"""
|
||||
|
||||
def __init__(self, using):
|
||||
self.using = using
|
||||
|
||||
def _legacy_enter_transaction_management(self, connection):
|
||||
if not connection.in_atomic_block:
|
||||
if connection.transaction_state and connection.transaction_state[-1]:
|
||||
connection._atomic_forced_unmanaged = True
|
||||
connection.enter_transaction_management(managed=False)
|
||||
else:
|
||||
connection._atomic_forced_unmanaged = False
|
||||
|
||||
def _legacy_leave_transaction_management(self, connection):
|
||||
if not connection.in_atomic_block and connection._atomic_forced_unmanaged:
|
||||
connection.leave_transaction_management()
|
||||
|
||||
def __enter__(self):
|
||||
connection = get_connection(self.using)
|
||||
|
||||
# Ensure we have a connection to the database before testing
|
||||
# autocommit status.
|
||||
connection.ensure_connection()
|
||||
|
||||
# Remove this when the legacy transaction management goes away.
|
||||
self._legacy_enter_transaction_management(connection)
|
||||
|
||||
if not connection.in_atomic_block and not connection.autocommit:
|
||||
raise TransactionManagementError(
|
||||
"'atomic' cannot be used when autocommit is disabled.")
|
||||
|
||||
if connection.in_atomic_block:
|
||||
# We're already in a transaction; create a savepoint.
|
||||
sid = connection.savepoint()
|
||||
connection.savepoint_ids.append(sid)
|
||||
else:
|
||||
# We aren't in a transaction yet; create one.
|
||||
# The usual way to start a transaction is to turn autocommit off.
|
||||
# However, some database adapters (namely sqlite3) don't handle
|
||||
# transactions and savepoints properly when autocommit is off.
|
||||
# In such cases, start an explicit transaction instead, which has
|
||||
# the side-effect of disabling autocommit.
|
||||
if connection.features.autocommits_when_autocommit_is_off:
|
||||
connection._start_transaction_under_autocommit()
|
||||
connection.autocommit = False
|
||||
else:
|
||||
connection.set_autocommit(False)
|
||||
connection.in_atomic_block = True
|
||||
connection.savepoint_ids.append(None)
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
connection = get_connection(self.using)
|
||||
sid = connection.savepoint_ids.pop()
|
||||
if exc_value is None:
|
||||
if sid is None:
|
||||
# Commit transaction
|
||||
connection.in_atomic_block = False
|
||||
try:
|
||||
connection.commit()
|
||||
except DatabaseError:
|
||||
connection.rollback()
|
||||
# Remove this when the legacy transaction management goes away.
|
||||
self._legacy_leave_transaction_management(connection)
|
||||
raise
|
||||
finally:
|
||||
if connection.features.autocommits_when_autocommit_is_off:
|
||||
connection.autocommit = True
|
||||
else:
|
||||
connection.set_autocommit(True)
|
||||
else:
|
||||
# Release savepoint
|
||||
try:
|
||||
connection.savepoint_commit(sid)
|
||||
except DatabaseError:
|
||||
connection.savepoint_rollback(sid)
|
||||
# Remove this when the legacy transaction management goes away.
|
||||
self._legacy_leave_transaction_management(connection)
|
||||
raise
|
||||
else:
|
||||
if sid is None:
|
||||
# Roll back transaction
|
||||
connection.in_atomic_block = False
|
||||
try:
|
||||
connection.rollback()
|
||||
finally:
|
||||
if connection.features.autocommits_when_autocommit_is_off:
|
||||
connection.autocommit = True
|
||||
else:
|
||||
connection.set_autocommit(True)
|
||||
else:
|
||||
# Roll back to savepoint
|
||||
connection.savepoint_rollback(sid)
|
||||
|
||||
# Remove this when the legacy transaction management goes away.
|
||||
self._legacy_leave_transaction_management(connection)
|
||||
|
||||
|
||||
def __call__(self, func):
|
||||
@wraps(func)
|
||||
def inner(*args, **kwargs):
|
||||
with self:
|
||||
return func(*args, **kwargs)
|
||||
return inner
|
||||
|
||||
|
||||
def atomic(using=None):
|
||||
# Bare decorator: @atomic -- although the first argument is called
|
||||
# `using`, it's actually the function being decorated.
|
||||
if callable(using):
|
||||
return Atomic(DEFAULT_DB_ALIAS)(using)
|
||||
# Decorator: @atomic(...) or context manager: with atomic(...): ...
|
||||
else:
|
||||
return Atomic(using)
|
||||
|
||||
|
||||
############################################
|
||||
# Deprecated decorators / context managers #
|
||||
############################################
|
||||
|
||||
class Transaction(object):
|
||||
"""
|
||||
|
@ -279,7 +421,8 @@ def commit_on_success_unless_managed(using=None):
|
|||
"""
|
||||
Transitory API to preserve backwards-compatibility while refactoring.
|
||||
"""
|
||||
if get_autocommit(using):
|
||||
connection = get_connection(using)
|
||||
if connection.autocommit and not connection.in_atomic_block:
|
||||
return commit_on_success(using)
|
||||
else:
|
||||
def entering(using):
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
==============================
|
||||
Managing database transactions
|
||||
==============================
|
||||
=====================
|
||||
Database transactions
|
||||
=====================
|
||||
|
||||
.. module:: django.db.transaction
|
||||
|
||||
Django gives you a few ways to control how database transactions are managed.
|
||||
|
||||
Managing database transactions
|
||||
==============================
|
||||
|
||||
Django's default transaction behavior
|
||||
=====================================
|
||||
-------------------------------------
|
||||
|
||||
Django's default behavior is to run in autocommit mode. Each query is
|
||||
immediately committed to the database. :ref:`See below for details
|
||||
|
@ -24,7 +27,7 @@ immediately committed to the database. :ref:`See below for details
|
|||
behavior <transactions-changes-from-1.5>`.
|
||||
|
||||
Tying transactions to HTTP requests
|
||||
===================================
|
||||
-----------------------------------
|
||||
|
||||
The recommended way to handle transactions in Web requests is to tie them to
|
||||
the request and response phases via Django's ``TransactionMiddleware``.
|
||||
|
@ -63,6 +66,85 @@ connection internally.
|
|||
multiple databases and want transaction control over databases other than
|
||||
"default", you will need to write your own transaction middleware.
|
||||
|
||||
Controlling transactions explicitly
|
||||
-----------------------------------
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
Django provides a single API to control database transactions.
|
||||
|
||||
.. function:: atomic(using=None)
|
||||
|
||||
This function creates an atomic block for writes to the database.
|
||||
(Atomicity is the defining property of database transactions.)
|
||||
|
||||
When the block completes successfully, the changes are committed to the
|
||||
database. When it raises an exception, the changes are rolled back.
|
||||
|
||||
``atomic`` can be nested. In this case, when an inner block completes
|
||||
successfully, its effects can still be rolled back if an exception is
|
||||
raised in the outer block at a later point.
|
||||
|
||||
``atomic`` takes a ``using`` argument which should be the name of a
|
||||
database. If this argument isn't provided, Django uses the ``"default"``
|
||||
database.
|
||||
|
||||
``atomic`` is usable both as a decorator::
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
@transaction.atomic
|
||||
def viewfunc(request):
|
||||
# This code executes inside a transaction.
|
||||
do_stuff()
|
||||
|
||||
and as a context manager::
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
def viewfunc(request):
|
||||
# This code executes in autocommit mode (Django's default).
|
||||
do_stuff()
|
||||
|
||||
with transaction.atomic():
|
||||
# This code executes inside a transaction.
|
||||
do_more_stuff()
|
||||
|
||||
Wrapping ``atomic`` in a try/except block allows for natural handling of
|
||||
integrity errors::
|
||||
|
||||
from django.db import IntegrityError, transaction
|
||||
|
||||
@transaction.atomic
|
||||
def viewfunc(request):
|
||||
do_stuff()
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
do_stuff_that_could_fail()
|
||||
except IntegrityError:
|
||||
handle_exception()
|
||||
|
||||
do_more_stuff()
|
||||
|
||||
In this example, even if ``do_stuff_that_could_fail()`` causes a database
|
||||
error by breaking an integrity constraint, you can execute queries in
|
||||
``do_more_stuff()``, and the changes from ``do_stuff()`` are still there.
|
||||
|
||||
In order to guarantee atomicity, ``atomic`` disables some APIs. Attempting
|
||||
to commit, roll back, or change the autocommit state of the database
|
||||
connection within an ``atomic`` block will raise an exception.
|
||||
|
||||
``atomic`` can only be used in autocommit mode. It will raise an exception
|
||||
if autocommit is turned off.
|
||||
|
||||
Under the hood, Django's transaction management code:
|
||||
|
||||
- opens a transaction when entering the outermost ``atomic`` block;
|
||||
- creates a savepoint when entering an inner ``atomic`` block;
|
||||
- releases or rolls back to the savepoint when exiting an inner block;
|
||||
- commits or rolls back the transaction when exiting the outermost block.
|
||||
|
||||
.. _transaction-management-functions:
|
||||
|
||||
Controlling transaction management in views
|
||||
|
@ -325,9 +407,8 @@ When autocommit is enabled, savepoints don't make sense. When it's disabled,
|
|||
commits before any statement other than ``SELECT``, ``INSERT``, ``UPDATE``,
|
||||
``DELETE`` and ``REPLACE``.)
|
||||
|
||||
As a consequence, savepoints are only usable if you start a transaction
|
||||
manually while in autocommit mode, and Django doesn't provide an API to
|
||||
achieve that.
|
||||
As a consequence, savepoints are only usable inside a transaction ie. inside
|
||||
an :func:`atomic` block.
|
||||
|
||||
Transactions in MySQL
|
||||
---------------------
|
||||
|
|
|
@ -22,4 +22,4 @@ class Reporter(models.Model):
|
|||
ordering = ('first_name', 'last_name')
|
||||
|
||||
def __str__(self):
|
||||
return "%s %s" % (self.first_name, self.last_name)
|
||||
return ("%s %s" % (self.first_name, self.last_name)).strip()
|
||||
|
|
|
@ -1,11 +1,163 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
import sys
|
||||
|
||||
from django.db import connection, transaction, IntegrityError
|
||||
from django.test import TransactionTestCase, skipUnlessDBFeature
|
||||
from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature
|
||||
from django.utils import six
|
||||
from django.utils.unittest import skipUnless
|
||||
|
||||
from .models import Reporter
|
||||
|
||||
|
||||
@skipUnless(connection.features.uses_savepoints,
|
||||
"'atomic' requires transactions and savepoints.")
|
||||
class AtomicTests(TransactionTestCase):
|
||||
"""
|
||||
Tests for the atomic decorator and context manager.
|
||||
|
||||
The tests make assertions on internal attributes because there isn't a
|
||||
robust way to ask the database for its current transaction state.
|
||||
|
||||
Since the decorator syntax is converted into a context manager (see the
|
||||
implementation), there are only a few basic tests with the decorator
|
||||
syntax and the bulk of the tests use the context manager syntax.
|
||||
"""
|
||||
|
||||
def test_decorator_syntax_commit(self):
|
||||
@transaction.atomic
|
||||
def make_reporter():
|
||||
Reporter.objects.create(first_name="Tintin")
|
||||
make_reporter()
|
||||
self.assertQuerysetEqual(Reporter.objects.all(), ['<Reporter: Tintin>'])
|
||||
|
||||
def test_decorator_syntax_rollback(self):
|
||||
@transaction.atomic
|
||||
def make_reporter():
|
||||
Reporter.objects.create(first_name="Haddock")
|
||||
raise Exception("Oops, that's his last name")
|
||||
with six.assertRaisesRegex(self, Exception, "Oops"):
|
||||
make_reporter()
|
||||
self.assertQuerysetEqual(Reporter.objects.all(), [])
|
||||
|
||||
def test_alternate_decorator_syntax_commit(self):
|
||||
@transaction.atomic()
|
||||
def make_reporter():
|
||||
Reporter.objects.create(first_name="Tintin")
|
||||
make_reporter()
|
||||
self.assertQuerysetEqual(Reporter.objects.all(), ['<Reporter: Tintin>'])
|
||||
|
||||
def test_alternate_decorator_syntax_rollback(self):
|
||||
@transaction.atomic()
|
||||
def make_reporter():
|
||||
Reporter.objects.create(first_name="Haddock")
|
||||
raise Exception("Oops, that's his last name")
|
||||
with six.assertRaisesRegex(self, Exception, "Oops"):
|
||||
make_reporter()
|
||||
self.assertQuerysetEqual(Reporter.objects.all(), [])
|
||||
|
||||
def test_commit(self):
|
||||
with transaction.atomic():
|
||||
Reporter.objects.create(first_name="Tintin")
|
||||
self.assertQuerysetEqual(Reporter.objects.all(), ['<Reporter: Tintin>'])
|
||||
|
||||
def test_rollback(self):
|
||||
with six.assertRaisesRegex(self, Exception, "Oops"):
|
||||
with transaction.atomic():
|
||||
Reporter.objects.create(first_name="Haddock")
|
||||
raise Exception("Oops, that's his last name")
|
||||
self.assertQuerysetEqual(Reporter.objects.all(), [])
|
||||
|
||||
def test_nested_commit_commit(self):
|
||||
with transaction.atomic():
|
||||
Reporter.objects.create(first_name="Tintin")
|
||||
with transaction.atomic():
|
||||
Reporter.objects.create(first_name="Archibald", last_name="Haddock")
|
||||
self.assertQuerysetEqual(Reporter.objects.all(),
|
||||
['<Reporter: Archibald Haddock>', '<Reporter: Tintin>'])
|
||||
|
||||
def test_nested_commit_rollback(self):
|
||||
with transaction.atomic():
|
||||
Reporter.objects.create(first_name="Tintin")
|
||||
with six.assertRaisesRegex(self, Exception, "Oops"):
|
||||
with transaction.atomic():
|
||||
Reporter.objects.create(first_name="Haddock")
|
||||
raise Exception("Oops, that's his last name")
|
||||
self.assertQuerysetEqual(Reporter.objects.all(), ['<Reporter: Tintin>'])
|
||||
|
||||
def test_nested_rollback_commit(self):
|
||||
with six.assertRaisesRegex(self, Exception, "Oops"):
|
||||
with transaction.atomic():
|
||||
Reporter.objects.create(last_name="Tintin")
|
||||
with transaction.atomic():
|
||||
Reporter.objects.create(last_name="Haddock")
|
||||
raise Exception("Oops, that's his first name")
|
||||
self.assertQuerysetEqual(Reporter.objects.all(), [])
|
||||
|
||||
def test_nested_rollback_rollback(self):
|
||||
with six.assertRaisesRegex(self, Exception, "Oops"):
|
||||
with transaction.atomic():
|
||||
Reporter.objects.create(last_name="Tintin")
|
||||
with six.assertRaisesRegex(self, Exception, "Oops"):
|
||||
with transaction.atomic():
|
||||
Reporter.objects.create(first_name="Haddock")
|
||||
raise Exception("Oops, that's his last name")
|
||||
raise Exception("Oops, that's his first name")
|
||||
self.assertQuerysetEqual(Reporter.objects.all(), [])
|
||||
|
||||
def test_reuse_commit_commit(self):
|
||||
atomic = transaction.atomic()
|
||||
with atomic:
|
||||
Reporter.objects.create(first_name="Tintin")
|
||||
with atomic:
|
||||
Reporter.objects.create(first_name="Archibald", last_name="Haddock")
|
||||
self.assertQuerysetEqual(Reporter.objects.all(),
|
||||
['<Reporter: Archibald Haddock>', '<Reporter: Tintin>'])
|
||||
|
||||
def test_reuse_commit_rollback(self):
|
||||
atomic = transaction.atomic()
|
||||
with atomic:
|
||||
Reporter.objects.create(first_name="Tintin")
|
||||
with six.assertRaisesRegex(self, Exception, "Oops"):
|
||||
with atomic:
|
||||
Reporter.objects.create(first_name="Haddock")
|
||||
raise Exception("Oops, that's his last name")
|
||||
self.assertQuerysetEqual(Reporter.objects.all(), ['<Reporter: Tintin>'])
|
||||
|
||||
def test_reuse_rollback_commit(self):
|
||||
atomic = transaction.atomic()
|
||||
with six.assertRaisesRegex(self, Exception, "Oops"):
|
||||
with atomic:
|
||||
Reporter.objects.create(last_name="Tintin")
|
||||
with atomic:
|
||||
Reporter.objects.create(last_name="Haddock")
|
||||
raise Exception("Oops, that's his first name")
|
||||
self.assertQuerysetEqual(Reporter.objects.all(), [])
|
||||
|
||||
def test_reuse_rollback_rollback(self):
|
||||
atomic = transaction.atomic()
|
||||
with six.assertRaisesRegex(self, Exception, "Oops"):
|
||||
with atomic:
|
||||
Reporter.objects.create(last_name="Tintin")
|
||||
with six.assertRaisesRegex(self, Exception, "Oops"):
|
||||
with atomic:
|
||||
Reporter.objects.create(first_name="Haddock")
|
||||
raise Exception("Oops, that's his last name")
|
||||
raise Exception("Oops, that's his first name")
|
||||
self.assertQuerysetEqual(Reporter.objects.all(), [])
|
||||
|
||||
|
||||
class AtomicInsideTransactionTests(AtomicTests):
|
||||
"""All basic tests for atomic should also pass within an existing transaction."""
|
||||
|
||||
def setUp(self):
|
||||
self.atomic = transaction.atomic()
|
||||
self.atomic.__enter__()
|
||||
|
||||
def tearDown(self):
|
||||
self.atomic.__exit__(*sys.exc_info())
|
||||
|
||||
|
||||
class TransactionTests(TransactionTestCase):
|
||||
def create_a_reporter_then_fail(self, first, last):
|
||||
a = Reporter(first_name=first, last_name=last)
|
||||
|
|
Loading…
Reference in New Issue