mirror of
https://github.com/django/django.git
synced 2025-11-07 07:15:35 +00:00
Made atomic usable when autocommit is off.
Thanks Anssi for haggling until I implemented this. This change alleviates the need for atomic_if_autocommit. When autocommit is disabled for a database, atomic will simply create and release savepoints, and not commit anything. This honors the contract of not doing any transaction management. This change also makes the hack to allow using atomic within the legacy transaction management redundant. None of the above will work with SQLite, because of a flaw in the design of the sqlite3 library. This is a known limitation that cannot be lifted without unacceptable side effects eg. triggering arbitrary commits.
This commit is contained in:
@@ -206,18 +206,6 @@ class Atomic(object):
|
||||
self.using = using
|
||||
self.savepoint = savepoint
|
||||
|
||||
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)
|
||||
|
||||
@@ -225,12 +213,31 @@ class Atomic(object):
|
||||
# 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 not connection.in_atomic_block:
|
||||
# Reset state when entering an outermost atomic block.
|
||||
connection.commit_on_exit = True
|
||||
connection.needs_rollback = False
|
||||
if not connection.autocommit:
|
||||
# Some database adapters (namely sqlite3) don't handle
|
||||
# transactions and savepoints properly when autocommit is off.
|
||||
# Turning autocommit back on isn't an option; it would trigger
|
||||
# a premature commit. Give up if that happens.
|
||||
if connection.features.autocommits_when_autocommit_is_off:
|
||||
raise TransactionManagementError(
|
||||
"Your database backend doesn't behave properly when "
|
||||
"autocommit is off. Turn it on before using 'atomic'.")
|
||||
# When entering an atomic block with autocommit turned off,
|
||||
# Django should only use savepoints and shouldn't commit.
|
||||
# This requires at least a savepoint for the outermost block.
|
||||
if not self.savepoint:
|
||||
raise TransactionManagementError(
|
||||
"The outermost 'atomic' block cannot use "
|
||||
"savepoint = False when autocommit is off.")
|
||||
# Pretend we're already in an atomic block to bypass the code
|
||||
# that disables autocommit to enter a transaction, and make a
|
||||
# note to deal with this case in __exit__.
|
||||
connection.in_atomic_block = True
|
||||
connection.commit_on_exit = False
|
||||
|
||||
if connection.in_atomic_block:
|
||||
# We're already in a transaction; create a savepoint, unless we
|
||||
@@ -255,63 +262,58 @@ class Atomic(object):
|
||||
else:
|
||||
connection.set_autocommit(False)
|
||||
connection.in_atomic_block = True
|
||||
connection.needs_rollback = False
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
connection = get_connection(self.using)
|
||||
if exc_value is None and not connection.needs_rollback:
|
||||
if connection.savepoint_ids:
|
||||
# Release savepoint if there is one
|
||||
sid = connection.savepoint_ids.pop()
|
||||
if sid is not None:
|
||||
|
||||
if connection.savepoint_ids:
|
||||
sid = connection.savepoint_ids.pop()
|
||||
else:
|
||||
# Prematurely unset this flag to allow using commit or rollback.
|
||||
connection.in_atomic_block = False
|
||||
|
||||
try:
|
||||
if exc_value is None and not connection.needs_rollback:
|
||||
if connection.in_atomic_block:
|
||||
# Release savepoint if there is one
|
||||
if sid is not None:
|
||||
try:
|
||||
connection.savepoint_commit(sid)
|
||||
except DatabaseError:
|
||||
connection.savepoint_rollback(sid)
|
||||
raise
|
||||
else:
|
||||
# Commit transaction
|
||||
try:
|
||||
connection.savepoint_commit(sid)
|
||||
connection.commit()
|
||||
except DatabaseError:
|
||||
connection.savepoint_rollback(sid)
|
||||
# Remove this when the legacy transaction management goes away.
|
||||
self._legacy_leave_transaction_management(connection)
|
||||
connection.rollback()
|
||||
raise
|
||||
else:
|
||||
# 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
|
||||
# This flag will be set to True again if there isn't a savepoint
|
||||
# allowing to perform the rollback at this level.
|
||||
connection.needs_rollback = False
|
||||
if connection.in_atomic_block:
|
||||
# Roll back to savepoint if there is one, mark for rollback
|
||||
# otherwise.
|
||||
if sid is None:
|
||||
connection.needs_rollback = True
|
||||
else:
|
||||
connection.set_autocommit(True)
|
||||
else:
|
||||
# This flag will be set to True again if there isn't a savepoint
|
||||
# allowing to perform the rollback at this level.
|
||||
connection.needs_rollback = False
|
||||
if connection.savepoint_ids:
|
||||
# Roll back to savepoint if there is one, mark for rollback
|
||||
# otherwise.
|
||||
sid = connection.savepoint_ids.pop()
|
||||
if sid is None:
|
||||
connection.needs_rollback = True
|
||||
connection.savepoint_rollback(sid)
|
||||
else:
|
||||
connection.savepoint_rollback(sid)
|
||||
else:
|
||||
# Roll back transaction
|
||||
connection.in_atomic_block = False
|
||||
try:
|
||||
# Roll back transaction
|
||||
connection.rollback()
|
||||
finally:
|
||||
if connection.features.autocommits_when_autocommit_is_off:
|
||||
connection.autocommit = True
|
||||
else:
|
||||
connection.set_autocommit(True)
|
||||
|
||||
# Remove this when the legacy transaction management goes away.
|
||||
self._legacy_leave_transaction_management(connection)
|
||||
|
||||
finally:
|
||||
# Outermost block exit when autocommit was enabled.
|
||||
if not connection.in_atomic_block:
|
||||
if connection.features.autocommits_when_autocommit_is_off:
|
||||
connection.autocommit = True
|
||||
else:
|
||||
connection.set_autocommit(True)
|
||||
# Outermost block exit when autocommit was disabled.
|
||||
elif not connection.savepoint_ids and not connection.commit_on_exit:
|
||||
connection.in_atomic_block = False
|
||||
|
||||
def __call__(self, func):
|
||||
@wraps(func, assigned=available_attrs(func))
|
||||
@@ -331,24 +333,6 @@ def atomic(using=None, savepoint=True):
|
||||
return Atomic(using, savepoint)
|
||||
|
||||
|
||||
def atomic_if_autocommit(using=None, savepoint=True):
|
||||
# This variant only exists to support the ability to disable transaction
|
||||
# management entirely in the DATABASES setting. It doesn't care about the
|
||||
# autocommit state at run time.
|
||||
db = DEFAULT_DB_ALIAS if callable(using) else using
|
||||
autocommit = get_connection(db).settings_dict['AUTOCOMMIT']
|
||||
|
||||
if autocommit:
|
||||
return atomic(using, savepoint)
|
||||
else:
|
||||
# Bare decorator: @atomic_if_autocommit
|
||||
if callable(using):
|
||||
return using
|
||||
# Decorator: @atomic_if_autocommit(...)
|
||||
else:
|
||||
return lambda func: func
|
||||
|
||||
|
||||
############################################
|
||||
# Deprecated decorators / context managers #
|
||||
############################################
|
||||
@@ -472,16 +456,15 @@ def commit_on_success_unless_managed(using=None, savepoint=False):
|
||||
Transitory API to preserve backwards-compatibility while refactoring.
|
||||
|
||||
Once the legacy transaction management is fully deprecated, this should
|
||||
simply be replaced by atomic_if_autocommit. Until then, it's necessary to
|
||||
avoid making a commit where Django didn't use to, since entering atomic in
|
||||
managed mode triggers a commmit.
|
||||
simply be replaced by atomic. Until then, it's necessary to guarantee that
|
||||
a commit occurs on exit, which atomic doesn't do when it's nested.
|
||||
|
||||
Unlike atomic, savepoint defaults to False because that's closer to the
|
||||
legacy behavior.
|
||||
"""
|
||||
connection = get_connection(using)
|
||||
if connection.autocommit or connection.in_atomic_block:
|
||||
return atomic_if_autocommit(using, savepoint)
|
||||
return atomic(using, savepoint)
|
||||
else:
|
||||
def entering(using):
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user