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>
|
Andreas Pelme <andreas@pelme.se>
|
||||||
permonik@mesias.brnonet.cz
|
permonik@mesias.brnonet.cz
|
||||||
peter@mymart.com
|
peter@mymart.com
|
||||||
|
Christophe Pettus <xof@thebuild.com>
|
||||||
pgross@thoughtworks.com
|
pgross@thoughtworks.com
|
||||||
phaedo <http://phaedo.cx/>
|
phaedo <http://phaedo.cx/>
|
||||||
phil@produxion.net
|
phil@produxion.net
|
||||||
|
|
|
@ -50,6 +50,12 @@ class BaseDatabaseWrapper(object):
|
||||||
# set somewhat aggressively, as the DBAPI doesn't make it easy to
|
# set somewhat aggressively, as the DBAPI doesn't make it easy to
|
||||||
# deduce if the connection is in transaction or not.
|
# deduce if the connection is in transaction or not.
|
||||||
self._dirty = False
|
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
|
# Connection termination related attributes
|
||||||
self.close_at = None
|
self.close_at = None
|
||||||
|
@ -148,7 +154,7 @@ class BaseDatabaseWrapper(object):
|
||||||
|
|
||||||
def commit(self):
|
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.validate_thread_sharing()
|
||||||
self._commit()
|
self._commit()
|
||||||
|
@ -156,7 +162,7 @@ class BaseDatabaseWrapper(object):
|
||||||
|
|
||||||
def rollback(self):
|
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.validate_thread_sharing()
|
||||||
self._rollback()
|
self._rollback()
|
||||||
|
@ -447,6 +453,12 @@ class BaseDatabaseWrapper(object):
|
||||||
if must_close:
|
if must_close:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
def _start_transaction_under_autocommit(self):
|
||||||
|
"""
|
||||||
|
Only required when autocommits_when_autocommit_is_off = True.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class BaseDatabaseFeatures(object):
|
class BaseDatabaseFeatures(object):
|
||||||
allows_group_by_pk = False
|
allows_group_by_pk = False
|
||||||
|
@ -549,6 +561,10 @@ class BaseDatabaseFeatures(object):
|
||||||
# Support for the DISTINCT ON clause
|
# Support for the DISTINCT ON clause
|
||||||
can_distinct_on_fields = False
|
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):
|
def __init__(self, connection):
|
||||||
self.connection = connection
|
self.connection = connection
|
||||||
|
|
||||||
|
@ -931,6 +947,9 @@ class BaseDatabaseOperations(object):
|
||||||
return "BEGIN;"
|
return "BEGIN;"
|
||||||
|
|
||||||
def end_transaction_sql(self, success=True):
|
def end_transaction_sql(self, success=True):
|
||||||
|
"""
|
||||||
|
Returns the SQL statement required to end a transaction.
|
||||||
|
"""
|
||||||
if not success:
|
if not success:
|
||||||
return "ROLLBACK;"
|
return "ROLLBACK;"
|
||||||
return "COMMIT;"
|
return "COMMIT;"
|
||||||
|
|
|
@ -99,6 +99,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
||||||
supports_mixed_date_datetime_comparisons = False
|
supports_mixed_date_datetime_comparisons = False
|
||||||
has_bulk_insert = True
|
has_bulk_insert = True
|
||||||
can_combine_inserts_with_and_without_auto_increment_pk = False
|
can_combine_inserts_with_and_without_auto_increment_pk = False
|
||||||
|
autocommits_when_autocommit_is_off = True
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def uses_savepoints(self):
|
def uses_savepoints(self):
|
||||||
|
@ -360,10 +361,12 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
BaseDatabaseWrapper.close(self)
|
BaseDatabaseWrapper.close(self)
|
||||||
|
|
||||||
def _savepoint_allowed(self):
|
def _savepoint_allowed(self):
|
||||||
# When 'isolation_level' is None, Django doesn't provide a way to
|
# When 'isolation_level' is not None, sqlite3 commits before each
|
||||||
# create a transaction (yet) so savepoints can't be created. When it
|
# savepoint; it's a bug. When it is None, savepoints don't make sense
|
||||||
# isn't, sqlite3 commits before each savepoint -- it's a bug.
|
# because autocommit is enabled. The only exception is inside atomic
|
||||||
return False
|
# 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):
|
def _set_autocommit(self, autocommit):
|
||||||
if autocommit:
|
if autocommit:
|
||||||
|
@ -413,6 +416,14 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
def is_usable(self):
|
def is_usable(self):
|
||||||
return True
|
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')
|
FORMAT_QMARK_REGEX = re.compile(r'(?<!%)%s')
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ import warnings
|
||||||
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from django.db import connections, DEFAULT_DB_ALIAS
|
from django.db import connections, DatabaseError, DEFAULT_DB_ALIAS
|
||||||
|
|
||||||
|
|
||||||
class TransactionManagementError(Exception):
|
class TransactionManagementError(Exception):
|
||||||
|
@ -134,13 +134,13 @@ def rollback_unless_managed(using=None):
|
||||||
|
|
||||||
def commit(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()
|
get_connection(using).commit()
|
||||||
|
|
||||||
def rollback(using=None):
|
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()
|
get_connection(using).rollback()
|
||||||
|
|
||||||
|
@ -166,9 +166,151 @@ def savepoint_commit(sid, using=None):
|
||||||
"""
|
"""
|
||||||
get_connection(using).savepoint_commit(sid)
|
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):
|
class Transaction(object):
|
||||||
"""
|
"""
|
||||||
|
@ -279,7 +421,8 @@ def commit_on_success_unless_managed(using=None):
|
||||||
"""
|
"""
|
||||||
Transitory API to preserve backwards-compatibility while refactoring.
|
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)
|
return commit_on_success(using)
|
||||||
else:
|
else:
|
||||||
def entering(using):
|
def entering(using):
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
==============================
|
=====================
|
||||||
Managing database transactions
|
Database transactions
|
||||||
==============================
|
=====================
|
||||||
|
|
||||||
.. module:: django.db.transaction
|
.. module:: django.db.transaction
|
||||||
|
|
||||||
Django gives you a few ways to control how database transactions are managed.
|
Django gives you a few ways to control how database transactions are managed.
|
||||||
|
|
||||||
|
Managing database transactions
|
||||||
|
==============================
|
||||||
|
|
||||||
Django's default transaction behavior
|
Django's default transaction behavior
|
||||||
=====================================
|
-------------------------------------
|
||||||
|
|
||||||
Django's default behavior is to run in autocommit mode. Each query is
|
Django's default behavior is to run in autocommit mode. Each query is
|
||||||
immediately committed to the database. :ref:`See below for details
|
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>`.
|
behavior <transactions-changes-from-1.5>`.
|
||||||
|
|
||||||
Tying transactions to HTTP requests
|
Tying transactions to HTTP requests
|
||||||
===================================
|
-----------------------------------
|
||||||
|
|
||||||
The recommended way to handle transactions in Web requests is to tie them to
|
The recommended way to handle transactions in Web requests is to tie them to
|
||||||
the request and response phases via Django's ``TransactionMiddleware``.
|
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
|
multiple databases and want transaction control over databases other than
|
||||||
"default", you will need to write your own transaction middleware.
|
"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:
|
.. _transaction-management-functions:
|
||||||
|
|
||||||
Controlling transaction management in views
|
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``,
|
commits before any statement other than ``SELECT``, ``INSERT``, ``UPDATE``,
|
||||||
``DELETE`` and ``REPLACE``.)
|
``DELETE`` and ``REPLACE``.)
|
||||||
|
|
||||||
As a consequence, savepoints are only usable if you start a transaction
|
As a consequence, savepoints are only usable inside a transaction ie. inside
|
||||||
manually while in autocommit mode, and Django doesn't provide an API to
|
an :func:`atomic` block.
|
||||||
achieve that.
|
|
||||||
|
|
||||||
Transactions in MySQL
|
Transactions in MySQL
|
||||||
---------------------
|
---------------------
|
||||||
|
|
|
@ -22,4 +22,4 @@ class Reporter(models.Model):
|
||||||
ordering = ('first_name', 'last_name')
|
ordering = ('first_name', 'last_name')
|
||||||
|
|
||||||
def __str__(self):
|
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
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
from django.db import connection, transaction, IntegrityError
|
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
|
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):
|
class TransactionTests(TransactionTestCase):
|
||||||
def create_a_reporter_then_fail(self, first, last):
|
def create_a_reporter_then_fail(self, first, last):
|
||||||
a = Reporter(first_name=first, last_name=last)
|
a = Reporter(first_name=first, last_name=last)
|
||||||
|
|
Loading…
Reference in New Issue