mirror of
				https://github.com/django/django.git
				synced 2025-11-04 13:39:16 +00:00 
			
		
		
		
	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
					
				
					 7 changed files with 430 additions and 23 deletions
				
			
		
							
								
								
									
										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…
	
	Add table
		Add a link
		
	
		Reference in a new issue