Merged branch 'database-level-autocommit'.
Fixed #2227: `atomic` supports nesting. Fixed #6623: `commit_manually` is deprecated and `atomic` doesn't suffer from this defect. Fixed #8320: the problem wasn't identified, but the legacy transaction management is deprecated. Fixed #10744: the problem wasn't identified, but the legacy transaction management is deprecated. Fixed #10813: since autocommit is enabled, it isn't necessary to rollback after errors any more. Fixed #13742: savepoints are now implemented for SQLite. Fixed #13870: transaction management in long running processes isn't a problem any more, and it's documented. Fixed #14970: while it digresses on transaction management, this ticket essentially asks for autocommit on PostgreSQL. Fixed #15694: `atomic` supports nesting. Fixed #17887: autocommit makes it impossible for a connection to stay "idle of transaction".
This commit is contained in:
commit
14cddf51c5
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
|
||||
|
|
|
@ -555,10 +555,6 @@ class LayerMapping(object):
|
|||
except SystemExit:
|
||||
raise
|
||||
except Exception as msg:
|
||||
if self.transaction_mode == 'autocommit':
|
||||
# Rolling back the transaction so that other model saves
|
||||
# will work.
|
||||
transaction.rollback_unless_managed()
|
||||
if strict:
|
||||
# Bailing out if the `strict` keyword is set.
|
||||
if not silent:
|
||||
|
|
|
@ -74,7 +74,6 @@ class SessionStore(SessionBase):
|
|||
@classmethod
|
||||
def clear_expired(cls):
|
||||
Session.objects.filter(expire_date__lt=timezone.now()).delete()
|
||||
transaction.commit_unless_managed()
|
||||
|
||||
|
||||
# At bottom to avoid circular import
|
||||
|
|
|
@ -10,7 +10,7 @@ except ImportError:
|
|||
|
||||
from django.conf import settings
|
||||
from django.core.cache.backends.base import BaseCache
|
||||
from django.db import connections, router, transaction, DatabaseError
|
||||
from django.db import connections, router, DatabaseError
|
||||
from django.utils import timezone, six
|
||||
from django.utils.encoding import force_bytes
|
||||
|
||||
|
@ -70,7 +70,6 @@ class DatabaseCache(BaseDatabaseCache):
|
|||
cursor = connections[db].cursor()
|
||||
cursor.execute("DELETE FROM %s "
|
||||
"WHERE cache_key = %%s" % table, [key])
|
||||
transaction.commit_unless_managed(using=db)
|
||||
return default
|
||||
value = connections[db].ops.process_clob(row[1])
|
||||
return pickle.loads(base64.b64decode(force_bytes(value)))
|
||||
|
@ -124,10 +123,8 @@ class DatabaseCache(BaseDatabaseCache):
|
|||
[key, b64encoded, connections[db].ops.value_to_db_datetime(exp)])
|
||||
except DatabaseError:
|
||||
# To be threadsafe, updates/inserts are allowed to fail silently
|
||||
transaction.rollback_unless_managed(using=db)
|
||||
return False
|
||||
else:
|
||||
transaction.commit_unless_managed(using=db)
|
||||
return True
|
||||
|
||||
def delete(self, key, version=None):
|
||||
|
@ -139,7 +136,6 @@ class DatabaseCache(BaseDatabaseCache):
|
|||
cursor = connections[db].cursor()
|
||||
|
||||
cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % table, [key])
|
||||
transaction.commit_unless_managed(using=db)
|
||||
|
||||
def has_key(self, key, version=None):
|
||||
key = self.make_key(key, version=version)
|
||||
|
@ -184,7 +180,6 @@ class DatabaseCache(BaseDatabaseCache):
|
|||
table = connections[db].ops.quote_name(self._table)
|
||||
cursor = connections[db].cursor()
|
||||
cursor.execute('DELETE FROM %s' % table)
|
||||
transaction.commit_unless_managed(using=db)
|
||||
|
||||
# For backwards compatibility
|
||||
class CacheClass(DatabaseCache):
|
||||
|
|
|
@ -6,10 +6,10 @@ import types
|
|||
|
||||
from django import http
|
||||
from django.conf import settings
|
||||
from django.core import exceptions
|
||||
from django.core import urlresolvers
|
||||
from django.core import signals
|
||||
from django.core.exceptions import MiddlewareNotUsed, PermissionDenied
|
||||
from django.db import connections, transaction
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.module_loading import import_by_path
|
||||
from django.utils import six
|
||||
|
@ -65,6 +65,13 @@ class BaseHandler(object):
|
|||
# as a flag for initialization being complete.
|
||||
self._request_middleware = request_middleware
|
||||
|
||||
def make_view_atomic(self, view):
|
||||
if getattr(view, 'transactions_per_request', True):
|
||||
for db in connections.all():
|
||||
if db.settings_dict['ATOMIC_REQUESTS']:
|
||||
view = transaction.atomic(using=db.alias)(view)
|
||||
return view
|
||||
|
||||
def get_response(self, request):
|
||||
"Returns an HttpResponse object for the given HttpRequest"
|
||||
try:
|
||||
|
@ -101,8 +108,9 @@ class BaseHandler(object):
|
|||
break
|
||||
|
||||
if response is None:
|
||||
wrapped_callback = self.make_view_atomic(callback)
|
||||
try:
|
||||
response = callback(request, *callback_args, **callback_kwargs)
|
||||
response = wrapped_callback(request, *callback_args, **callback_kwargs)
|
||||
except Exception as e:
|
||||
# If the view raised an exception, run it through exception
|
||||
# middleware, and if the exception middleware returns a
|
||||
|
|
|
@ -53,14 +53,13 @@ class Command(LabelCommand):
|
|||
for i, line in enumerate(table_output):
|
||||
full_statement.append(' %s%s' % (line, i < len(table_output)-1 and ',' or ''))
|
||||
full_statement.append(');')
|
||||
curs = connection.cursor()
|
||||
try:
|
||||
curs.execute("\n".join(full_statement))
|
||||
except DatabaseError as e:
|
||||
transaction.rollback_unless_managed(using=db)
|
||||
raise CommandError(
|
||||
"Cache table '%s' could not be created.\nThe error was: %s." %
|
||||
(tablename, force_text(e)))
|
||||
for statement in index_output:
|
||||
curs.execute(statement)
|
||||
transaction.commit_unless_managed(using=db)
|
||||
with transaction.commit_on_success_unless_managed():
|
||||
curs = connection.cursor()
|
||||
try:
|
||||
curs.execute("\n".join(full_statement))
|
||||
except DatabaseError as e:
|
||||
raise CommandError(
|
||||
"Cache table '%s' could not be created.\nThe error was: %s." %
|
||||
(tablename, force_text(e)))
|
||||
for statement in index_output:
|
||||
curs.execute(statement)
|
||||
|
|
|
@ -57,18 +57,17 @@ Are you sure you want to do this?
|
|||
|
||||
if confirm == 'yes':
|
||||
try:
|
||||
cursor = connection.cursor()
|
||||
for sql in sql_list:
|
||||
cursor.execute(sql)
|
||||
with transaction.commit_on_success_unless_managed():
|
||||
cursor = connection.cursor()
|
||||
for sql in sql_list:
|
||||
cursor.execute(sql)
|
||||
except Exception as e:
|
||||
transaction.rollback_unless_managed(using=db)
|
||||
raise CommandError("""Database %s couldn't be flushed. Possible reasons:
|
||||
* The database isn't running or isn't configured correctly.
|
||||
* At least one of the expected database tables doesn't exist.
|
||||
* The SQL was invalid.
|
||||
Hint: Look at the output of 'django-admin.py sqlflush'. That's the SQL this command wasn't able to run.
|
||||
The full error: %s""" % (connection.settings_dict['NAME'], e))
|
||||
transaction.commit_unless_managed(using=db)
|
||||
|
||||
# Emit the post sync signal. This allows individual
|
||||
# applications to respond as if the database had been
|
||||
|
|
|
@ -41,8 +41,6 @@ class Command(BaseCommand):
|
|||
self.ignore = options.get('ignore')
|
||||
self.using = options.get('database')
|
||||
|
||||
connection = connections[self.using]
|
||||
|
||||
if not len(fixture_labels):
|
||||
raise CommandError(
|
||||
"No database fixture specified. Please provide the path of at "
|
||||
|
@ -51,13 +49,18 @@ class Command(BaseCommand):
|
|||
|
||||
self.verbosity = int(options.get('verbosity'))
|
||||
|
||||
# commit is a stealth option - it isn't really useful as
|
||||
# a command line option, but it can be useful when invoking
|
||||
# loaddata from within another script.
|
||||
# If commit=True, loaddata will use its own transaction;
|
||||
# if commit=False, the data load SQL will become part of
|
||||
# the transaction in place when loaddata was invoked.
|
||||
commit = options.get('commit', True)
|
||||
with transaction.commit_on_success_unless_managed(using=self.using):
|
||||
self.loaddata(fixture_labels)
|
||||
|
||||
# Close the DB connection -- unless we're still in a transaction. This
|
||||
# is required as a workaround for an edge case in MySQL: if the same
|
||||
# connection is used to create tables, load data, and query, the query
|
||||
# can return incorrect results. See Django #7572, MySQL #37735.
|
||||
if transaction.get_autocommit(self.using):
|
||||
connections[self.using].close()
|
||||
|
||||
def loaddata(self, fixture_labels):
|
||||
connection = connections[self.using]
|
||||
|
||||
# Keep a count of the installed objects and fixtures
|
||||
self.fixture_count = 0
|
||||
|
@ -65,18 +68,6 @@ class Command(BaseCommand):
|
|||
self.fixture_object_count = 0
|
||||
self.models = set()
|
||||
|
||||
# Get a cursor (even though we don't need one yet). This has
|
||||
# the side effect of initializing the test database (if
|
||||
# it isn't already initialized).
|
||||
cursor = connection.cursor()
|
||||
|
||||
# Start transaction management. All fixtures are installed in a
|
||||
# single transaction to ensure that all references are resolved.
|
||||
if commit:
|
||||
transaction.commit_unless_managed(using=self.using)
|
||||
transaction.enter_transaction_management(using=self.using)
|
||||
transaction.managed(True, using=self.using)
|
||||
|
||||
class SingleZipReader(zipfile.ZipFile):
|
||||
def __init__(self, *args, **kwargs):
|
||||
zipfile.ZipFile.__init__(self, *args, **kwargs)
|
||||
|
@ -105,26 +96,17 @@ class Command(BaseCommand):
|
|||
|
||||
app_fixtures = [os.path.join(os.path.dirname(path), 'fixtures') for path in app_module_paths]
|
||||
|
||||
with connection.constraint_checks_disabled():
|
||||
for fixture_label in fixture_labels:
|
||||
self.load_label(fixture_label, app_fixtures)
|
||||
|
||||
# Since we disabled constraint checks, we must manually check for
|
||||
# any invalid keys that might have been added
|
||||
table_names = [model._meta.db_table for model in self.models]
|
||||
try:
|
||||
with connection.constraint_checks_disabled():
|
||||
for fixture_label in fixture_labels:
|
||||
self.load_label(fixture_label, app_fixtures)
|
||||
|
||||
# Since we disabled constraint checks, we must manually check for
|
||||
# any invalid keys that might have been added
|
||||
table_names = [model._meta.db_table for model in self.models]
|
||||
try:
|
||||
connection.check_constraints(table_names=table_names)
|
||||
except Exception as e:
|
||||
e.args = ("Problem installing fixtures: %s" % e,)
|
||||
raise
|
||||
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
raise
|
||||
connection.check_constraints(table_names=table_names)
|
||||
except Exception as e:
|
||||
if commit:
|
||||
transaction.rollback(using=self.using)
|
||||
transaction.leave_transaction_management(using=self.using)
|
||||
e.args = ("Problem installing fixtures: %s" % e,)
|
||||
raise
|
||||
|
||||
# If we found even one object in a fixture, we need to reset the
|
||||
|
@ -137,10 +119,6 @@ class Command(BaseCommand):
|
|||
for line in sequence_sql:
|
||||
cursor.execute(line)
|
||||
|
||||
if commit:
|
||||
transaction.commit(using=self.using)
|
||||
transaction.leave_transaction_management(using=self.using)
|
||||
|
||||
if self.verbosity >= 1:
|
||||
if self.fixture_object_count == self.loaded_object_count:
|
||||
self.stdout.write("Installed %d object(s) from %d fixture(s)" % (
|
||||
|
@ -149,13 +127,6 @@ class Command(BaseCommand):
|
|||
self.stdout.write("Installed %d object(s) (of %d) from %d fixture(s)" % (
|
||||
self.loaded_object_count, self.fixture_object_count, self.fixture_count))
|
||||
|
||||
# Close the DB connection. This is required as a workaround for an
|
||||
# edge case in MySQL: if the same connection is used to
|
||||
# create tables, load data, and query, the query can return
|
||||
# incorrect results. See Django #7572, MySQL #37735.
|
||||
if commit:
|
||||
connection.close()
|
||||
|
||||
def load_label(self, fixture_label, app_fixtures):
|
||||
|
||||
parts = fixture_label.split('.')
|
||||
|
|
|
@ -83,26 +83,25 @@ class Command(NoArgsCommand):
|
|||
# Create the tables for each model
|
||||
if verbosity >= 1:
|
||||
self.stdout.write("Creating tables ...\n")
|
||||
for app_name, model_list in manifest.items():
|
||||
for model in model_list:
|
||||
# Create the model's database table, if it doesn't already exist.
|
||||
if verbosity >= 3:
|
||||
self.stdout.write("Processing %s.%s model\n" % (app_name, model._meta.object_name))
|
||||
sql, references = connection.creation.sql_create_model(model, self.style, seen_models)
|
||||
seen_models.add(model)
|
||||
created_models.add(model)
|
||||
for refto, refs in references.items():
|
||||
pending_references.setdefault(refto, []).extend(refs)
|
||||
if refto in seen_models:
|
||||
sql.extend(connection.creation.sql_for_pending_references(refto, self.style, pending_references))
|
||||
sql.extend(connection.creation.sql_for_pending_references(model, self.style, pending_references))
|
||||
if verbosity >= 1 and sql:
|
||||
self.stdout.write("Creating table %s\n" % model._meta.db_table)
|
||||
for statement in sql:
|
||||
cursor.execute(statement)
|
||||
tables.append(connection.introspection.table_name_converter(model._meta.db_table))
|
||||
|
||||
transaction.commit_unless_managed(using=db)
|
||||
with transaction.commit_on_success_unless_managed(using=db):
|
||||
for app_name, model_list in manifest.items():
|
||||
for model in model_list:
|
||||
# Create the model's database table, if it doesn't already exist.
|
||||
if verbosity >= 3:
|
||||
self.stdout.write("Processing %s.%s model\n" % (app_name, model._meta.object_name))
|
||||
sql, references = connection.creation.sql_create_model(model, self.style, seen_models)
|
||||
seen_models.add(model)
|
||||
created_models.add(model)
|
||||
for refto, refs in references.items():
|
||||
pending_references.setdefault(refto, []).extend(refs)
|
||||
if refto in seen_models:
|
||||
sql.extend(connection.creation.sql_for_pending_references(refto, self.style, pending_references))
|
||||
sql.extend(connection.creation.sql_for_pending_references(model, self.style, pending_references))
|
||||
if verbosity >= 1 and sql:
|
||||
self.stdout.write("Creating table %s\n" % model._meta.db_table)
|
||||
for statement in sql:
|
||||
cursor.execute(statement)
|
||||
tables.append(connection.introspection.table_name_converter(model._meta.db_table))
|
||||
|
||||
# Send the post_syncdb signal, so individual apps can do whatever they need
|
||||
# to do at this point.
|
||||
|
@ -122,17 +121,16 @@ class Command(NoArgsCommand):
|
|||
if custom_sql:
|
||||
if verbosity >= 2:
|
||||
self.stdout.write("Installing custom SQL for %s.%s model\n" % (app_name, model._meta.object_name))
|
||||
try:
|
||||
for sql in custom_sql:
|
||||
cursor.execute(sql)
|
||||
except Exception as e:
|
||||
self.stderr.write("Failed to install custom SQL for %s.%s model: %s\n" % \
|
||||
(app_name, model._meta.object_name, e))
|
||||
if show_traceback:
|
||||
traceback.print_exc()
|
||||
transaction.rollback_unless_managed(using=db)
|
||||
else:
|
||||
transaction.commit_unless_managed(using=db)
|
||||
with transaction.commit_on_success_unless_managed(using=db):
|
||||
try:
|
||||
for sql in custom_sql:
|
||||
cursor.execute(sql)
|
||||
except Exception as e:
|
||||
self.stderr.write("Failed to install custom SQL for %s.%s model: %s\n" % \
|
||||
(app_name, model._meta.object_name, e))
|
||||
if show_traceback:
|
||||
traceback.print_exc()
|
||||
raise
|
||||
else:
|
||||
if verbosity >= 3:
|
||||
self.stdout.write("No custom SQL for %s.%s model\n" % (app_name, model._meta.object_name))
|
||||
|
@ -147,15 +145,14 @@ class Command(NoArgsCommand):
|
|||
if index_sql:
|
||||
if verbosity >= 2:
|
||||
self.stdout.write("Installing index for %s.%s model\n" % (app_name, model._meta.object_name))
|
||||
try:
|
||||
for sql in index_sql:
|
||||
cursor.execute(sql)
|
||||
except Exception as e:
|
||||
self.stderr.write("Failed to install index for %s.%s model: %s\n" % \
|
||||
(app_name, model._meta.object_name, e))
|
||||
transaction.rollback_unless_managed(using=db)
|
||||
else:
|
||||
transaction.commit_unless_managed(using=db)
|
||||
with transaction.commit_on_success_unless_managed(using=db):
|
||||
try:
|
||||
for sql in index_sql:
|
||||
cursor.execute(sql)
|
||||
except Exception as e:
|
||||
self.stderr.write("Failed to install index for %s.%s model: %s\n" % \
|
||||
(app_name, model._meta.object_name, e))
|
||||
raise
|
||||
|
||||
# Load initial_data fixtures (unless that has been disabled)
|
||||
if load_initial_data:
|
||||
|
|
|
@ -70,6 +70,7 @@ signals.request_started.connect(reset_queries)
|
|||
# their lifetime. NB: abort() doesn't do anything outside of a transaction.
|
||||
def close_old_connections(**kwargs):
|
||||
for conn in connections.all():
|
||||
# Remove this when the legacy transaction management goes away.
|
||||
try:
|
||||
conn.abort()
|
||||
except DatabaseError:
|
||||
|
@ -77,14 +78,3 @@ def close_old_connections(**kwargs):
|
|||
conn.close_if_unusable_or_obsolete()
|
||||
signals.request_started.connect(close_old_connections)
|
||||
signals.request_finished.connect(close_old_connections)
|
||||
|
||||
# Register an event that rolls back the connections
|
||||
# when a Django request has an exception.
|
||||
def _rollback_on_exception(**kwargs):
|
||||
from django.db import transaction
|
||||
for conn in connections:
|
||||
try:
|
||||
transaction.rollback_unless_managed(using=conn)
|
||||
except DatabaseError:
|
||||
pass
|
||||
signals.got_request_exception.connect(_rollback_on_exception)
|
||||
|
|
|
@ -44,11 +44,21 @@ class BaseDatabaseWrapper(object):
|
|||
self.savepoint_state = 0
|
||||
|
||||
# Transaction management related attributes
|
||||
self.autocommit = False
|
||||
self.transaction_state = []
|
||||
# Tracks if the connection is believed to be in transaction. This is
|
||||
# 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
|
||||
# Tracks if the transaction should be rolled back to the next
|
||||
# available savepoint because of an exception in an inner block.
|
||||
self.needs_rollback = 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
|
||||
|
@ -85,20 +95,35 @@ class BaseDatabaseWrapper(object):
|
|||
"""Creates a cursor. Assumes that a connection is established."""
|
||||
raise NotImplementedError
|
||||
|
||||
##### Backend-specific methods for creating connections #####
|
||||
|
||||
def connect(self):
|
||||
"""Connects to the database. Assumes that the connection is closed."""
|
||||
# Reset parameters defining when to close the connection
|
||||
max_age = self.settings_dict['CONN_MAX_AGE']
|
||||
self.close_at = None if max_age is None else time.time() + max_age
|
||||
self.errors_occurred = False
|
||||
# Establish the connection
|
||||
conn_params = self.get_connection_params()
|
||||
self.connection = self.get_new_connection(conn_params)
|
||||
self.init_connection_state()
|
||||
if self.settings_dict['AUTOCOMMIT']:
|
||||
self.set_autocommit(True)
|
||||
connection_created.send(sender=self.__class__, connection=self)
|
||||
|
||||
def ensure_connection(self):
|
||||
"""
|
||||
Guarantees that a connection to the database is established.
|
||||
"""
|
||||
if self.connection is None:
|
||||
with self.wrap_database_errors():
|
||||
self.connect()
|
||||
|
||||
##### Backend-specific wrappers for PEP-249 connection methods #####
|
||||
|
||||
def _cursor(self):
|
||||
self.ensure_connection()
|
||||
with self.wrap_database_errors():
|
||||
if self.connection is None:
|
||||
# Reset parameters defining when to close the connection
|
||||
max_age = self.settings_dict['CONN_MAX_AGE']
|
||||
self.close_at = None if max_age is None else time.time() + max_age
|
||||
self.errors_occurred = False
|
||||
# Establish the connection
|
||||
conn_params = self.get_connection_params()
|
||||
self.connection = self.get_new_connection(conn_params)
|
||||
self.init_connection_state()
|
||||
connection_created.send(sender=self.__class__, connection=self)
|
||||
return self.create_cursor()
|
||||
|
||||
def _commit(self):
|
||||
|
@ -132,17 +157,19 @@ 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.validate_no_atomic_block()
|
||||
self._commit()
|
||||
self.set_clean()
|
||||
|
||||
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_no_atomic_block()
|
||||
self._rollback()
|
||||
self.set_clean()
|
||||
|
||||
|
@ -160,54 +187,59 @@ class BaseDatabaseWrapper(object):
|
|||
##### Backend-specific savepoint management methods #####
|
||||
|
||||
def _savepoint(self, sid):
|
||||
if not self.features.uses_savepoints:
|
||||
return
|
||||
self.cursor().execute(self.ops.savepoint_create_sql(sid))
|
||||
|
||||
def _savepoint_rollback(self, sid):
|
||||
if not self.features.uses_savepoints:
|
||||
return
|
||||
self.cursor().execute(self.ops.savepoint_rollback_sql(sid))
|
||||
|
||||
def _savepoint_commit(self, sid):
|
||||
if not self.features.uses_savepoints:
|
||||
return
|
||||
self.cursor().execute(self.ops.savepoint_commit_sql(sid))
|
||||
|
||||
def _savepoint_allowed(self):
|
||||
# Savepoints cannot be created outside a transaction
|
||||
return self.features.uses_savepoints and not self.autocommit
|
||||
|
||||
##### Generic savepoint management methods #####
|
||||
|
||||
def savepoint(self):
|
||||
"""
|
||||
Creates a savepoint (if supported and required by the backend) inside the
|
||||
current transaction. Returns an identifier for the savepoint that will be
|
||||
used for the subsequent rollback or commit.
|
||||
Creates a savepoint inside the current transaction. Returns an
|
||||
identifier for the savepoint that will be used for the subsequent
|
||||
rollback or commit. Does nothing if savepoints are not supported.
|
||||
"""
|
||||
if not self._savepoint_allowed():
|
||||
return
|
||||
|
||||
thread_ident = thread.get_ident()
|
||||
tid = str(thread_ident).replace('-', '')
|
||||
|
||||
self.savepoint_state += 1
|
||||
|
||||
tid = str(thread_ident).replace('-', '')
|
||||
sid = "s%s_x%d" % (tid, self.savepoint_state)
|
||||
|
||||
self.validate_thread_sharing()
|
||||
self._savepoint(sid)
|
||||
|
||||
return sid
|
||||
|
||||
def savepoint_rollback(self, sid):
|
||||
"""
|
||||
Rolls back the most recent savepoint (if one exists). Does nothing if
|
||||
savepoints are not supported.
|
||||
Rolls back to a savepoint. Does nothing if savepoints are not supported.
|
||||
"""
|
||||
if not self._savepoint_allowed():
|
||||
return
|
||||
|
||||
self.validate_thread_sharing()
|
||||
if self.savepoint_state:
|
||||
self._savepoint_rollback(sid)
|
||||
self._savepoint_rollback(sid)
|
||||
|
||||
def savepoint_commit(self, sid):
|
||||
"""
|
||||
Commits the most recent savepoint (if one exists). Does nothing if
|
||||
savepoints are not supported.
|
||||
Releases a savepoint. Does nothing if savepoints are not supported.
|
||||
"""
|
||||
if not self._savepoint_allowed():
|
||||
return
|
||||
|
||||
self.validate_thread_sharing()
|
||||
if self.savepoint_state:
|
||||
self._savepoint_commit(sid)
|
||||
self._savepoint_commit(sid)
|
||||
|
||||
def clean_savepoints(self):
|
||||
"""
|
||||
|
@ -217,24 +249,15 @@ class BaseDatabaseWrapper(object):
|
|||
|
||||
##### Backend-specific transaction management methods #####
|
||||
|
||||
def _enter_transaction_management(self, managed):
|
||||
def _set_autocommit(self, autocommit):
|
||||
"""
|
||||
A hook for backend-specific changes required when entering manual
|
||||
transaction handling.
|
||||
Backend-specific implementation to enable or disable autocommit.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _leave_transaction_management(self, managed):
|
||||
"""
|
||||
A hook for backend-specific changes required when leaving manual
|
||||
transaction handling. Will usually be implemented only when
|
||||
_enter_transaction_management() is also required.
|
||||
"""
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
##### Generic transaction management methods #####
|
||||
|
||||
def enter_transaction_management(self, managed=True):
|
||||
def enter_transaction_management(self, managed=True, forced=False):
|
||||
"""
|
||||
Enters transaction management for a running thread. It must be balanced with
|
||||
the appropriate leave_transaction_management call, since the actual state is
|
||||
|
@ -243,12 +266,22 @@ class BaseDatabaseWrapper(object):
|
|||
The state and dirty flag are carried over from the surrounding block or
|
||||
from the settings, if there is no surrounding block (dirty is always false
|
||||
when no current block is running).
|
||||
|
||||
If you switch off transaction management and there is a pending
|
||||
commit/rollback, the data will be commited, unless "forced" is True.
|
||||
"""
|
||||
if self.transaction_state:
|
||||
self.transaction_state.append(self.transaction_state[-1])
|
||||
else:
|
||||
self.transaction_state.append(settings.TRANSACTIONS_MANAGED)
|
||||
self._enter_transaction_management(managed)
|
||||
self.validate_no_atomic_block()
|
||||
|
||||
self.ensure_connection()
|
||||
|
||||
self.transaction_state.append(managed)
|
||||
|
||||
if not managed and self.is_dirty() and not forced:
|
||||
self.commit()
|
||||
self.set_clean()
|
||||
|
||||
if managed == self.autocommit:
|
||||
self.set_autocommit(not managed)
|
||||
|
||||
def leave_transaction_management(self):
|
||||
"""
|
||||
|
@ -256,22 +289,48 @@ class BaseDatabaseWrapper(object):
|
|||
over to the surrounding block, as a commit will commit all changes, even
|
||||
those from outside. (Commits are on connection level.)
|
||||
"""
|
||||
self.validate_no_atomic_block()
|
||||
|
||||
self.ensure_connection()
|
||||
|
||||
if self.transaction_state:
|
||||
del self.transaction_state[-1]
|
||||
else:
|
||||
raise TransactionManagementError(
|
||||
"This code isn't under transaction management")
|
||||
# The _leave_transaction_management hook can change the dirty flag,
|
||||
# so memoize it.
|
||||
dirty = self._dirty
|
||||
# We will pass the next status (after leaving the previous state
|
||||
# behind) to subclass hook.
|
||||
self._leave_transaction_management(self.is_managed())
|
||||
if dirty:
|
||||
|
||||
if self.transaction_state:
|
||||
managed = self.transaction_state[-1]
|
||||
else:
|
||||
managed = not self.settings_dict['AUTOCOMMIT']
|
||||
|
||||
if self._dirty:
|
||||
self.rollback()
|
||||
if managed == self.autocommit:
|
||||
self.set_autocommit(not managed)
|
||||
raise TransactionManagementError(
|
||||
"Transaction managed block ended with pending COMMIT/ROLLBACK")
|
||||
|
||||
if managed == self.autocommit:
|
||||
self.set_autocommit(not managed)
|
||||
|
||||
def set_autocommit(self, autocommit):
|
||||
"""
|
||||
Enable or disable autocommit.
|
||||
"""
|
||||
self.validate_no_atomic_block()
|
||||
self.ensure_connection()
|
||||
self._set_autocommit(autocommit)
|
||||
self.autocommit = autocommit
|
||||
|
||||
def validate_no_atomic_block(self):
|
||||
"""
|
||||
Raise an error if an atomic block is active.
|
||||
"""
|
||||
if self.in_atomic_block:
|
||||
raise TransactionManagementError(
|
||||
"This is forbidden when an 'atomic' block is active.")
|
||||
|
||||
def abort(self):
|
||||
"""
|
||||
Roll back any ongoing transaction and clean the transaction state
|
||||
|
@ -295,7 +354,8 @@ class BaseDatabaseWrapper(object):
|
|||
to decide in a managed block of code to decide whether there are open
|
||||
changes waiting for commit.
|
||||
"""
|
||||
self._dirty = True
|
||||
if not self.autocommit:
|
||||
self._dirty = True
|
||||
|
||||
def set_clean(self):
|
||||
"""
|
||||
|
@ -306,51 +366,6 @@ class BaseDatabaseWrapper(object):
|
|||
self._dirty = False
|
||||
self.clean_savepoints()
|
||||
|
||||
def is_managed(self):
|
||||
"""
|
||||
Checks whether the transaction manager is in manual or in auto state.
|
||||
"""
|
||||
if self.transaction_state:
|
||||
return self.transaction_state[-1]
|
||||
return settings.TRANSACTIONS_MANAGED
|
||||
|
||||
def managed(self, flag=True):
|
||||
"""
|
||||
Puts the transaction manager into a manual state: managed transactions have
|
||||
to be committed explicitly by the user. If you switch off transaction
|
||||
management and there is a pending commit/rollback, the data will be
|
||||
commited.
|
||||
"""
|
||||
top = self.transaction_state
|
||||
if top:
|
||||
top[-1] = flag
|
||||
if not flag and self.is_dirty():
|
||||
self.commit()
|
||||
else:
|
||||
raise TransactionManagementError("This code isn't under transaction "
|
||||
"management")
|
||||
|
||||
def commit_unless_managed(self):
|
||||
"""
|
||||
Commits changes if the system is not in managed transaction mode.
|
||||
"""
|
||||
self.validate_thread_sharing()
|
||||
if not self.is_managed():
|
||||
self.commit()
|
||||
self.clean_savepoints()
|
||||
else:
|
||||
self.set_dirty()
|
||||
|
||||
def rollback_unless_managed(self):
|
||||
"""
|
||||
Rolls back changes if the system is not in managed transaction mode.
|
||||
"""
|
||||
self.validate_thread_sharing()
|
||||
if not self.is_managed():
|
||||
self.rollback()
|
||||
else:
|
||||
self.set_dirty()
|
||||
|
||||
##### Foreign key constraints checks handling #####
|
||||
|
||||
@contextmanager
|
||||
|
@ -402,12 +417,19 @@ class BaseDatabaseWrapper(object):
|
|||
or if it outlived its maximum age.
|
||||
"""
|
||||
if self.connection is not None:
|
||||
# If the application didn't restore the original autocommit setting,
|
||||
# don't take chances, drop the connection.
|
||||
if self.autocommit != self.settings_dict['AUTOCOMMIT']:
|
||||
self.close()
|
||||
return
|
||||
|
||||
if self.errors_occurred:
|
||||
if self.is_usable():
|
||||
self.errors_occurred = False
|
||||
else:
|
||||
self.close()
|
||||
return
|
||||
|
||||
if self.close_at is not None and time.time() >= self.close_at:
|
||||
self.close()
|
||||
return
|
||||
|
@ -460,6 +482,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
|
||||
|
@ -479,7 +507,6 @@ class BaseDatabaseFeatures(object):
|
|||
can_use_chunked_reads = True
|
||||
can_return_id_from_insert = False
|
||||
has_bulk_insert = False
|
||||
uses_autocommit = False
|
||||
uses_savepoints = False
|
||||
can_combine_inserts_with_and_without_auto_increment_pk = False
|
||||
|
||||
|
@ -563,6 +590,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
|
||||
|
||||
|
@ -574,7 +605,6 @@ class BaseDatabaseFeatures(object):
|
|||
# otherwise autocommit will cause the confimation to
|
||||
# fail.
|
||||
self.connection.enter_transaction_management()
|
||||
self.connection.managed(True)
|
||||
cursor = self.connection.cursor()
|
||||
cursor.execute('CREATE TABLE ROLLBACK_TEST (X INT)')
|
||||
self.connection.commit()
|
||||
|
@ -883,19 +913,19 @@ class BaseDatabaseOperations(object):
|
|||
"uses_savepoints" feature is True. The "sid" parameter is a string
|
||||
for the savepoint id.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
return "SAVEPOINT %s" % self.quote_name(sid)
|
||||
|
||||
def savepoint_commit_sql(self, sid):
|
||||
"""
|
||||
Returns the SQL for committing the given savepoint.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
return "RELEASE SAVEPOINT %s" % self.quote_name(sid)
|
||||
|
||||
def savepoint_rollback_sql(self, sid):
|
||||
"""
|
||||
Returns the SQL for rolling back the given savepoint.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
return "ROLLBACK TO SAVEPOINT %s" % self.quote_name(sid)
|
||||
|
||||
def set_time_zone_sql(self):
|
||||
"""
|
||||
|
@ -946,6 +976,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;"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import hashlib
|
||||
import sys
|
||||
import time
|
||||
import warnings
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.utils import load_backend
|
||||
|
@ -382,10 +383,7 @@ class BaseDatabaseCreation(object):
|
|||
|
||||
qn = self.connection.ops.quote_name
|
||||
|
||||
# Create the test database and connect to it. We need to autocommit
|
||||
# if the database supports it because PostgreSQL doesn't allow
|
||||
# CREATE/DROP DATABASE statements within transactions.
|
||||
self._prepare_for_test_db_ddl()
|
||||
# Create the test database and connect to it.
|
||||
cursor = self.connection.cursor()
|
||||
try:
|
||||
cursor.execute(
|
||||
|
@ -453,7 +451,6 @@ class BaseDatabaseCreation(object):
|
|||
# to do so, because it's not allowed to delete a database while being
|
||||
# connected to it.
|
||||
cursor = self.connection.cursor()
|
||||
self._prepare_for_test_db_ddl()
|
||||
# Wait to avoid "database is being accessed by other users" errors.
|
||||
time.sleep(1)
|
||||
cursor.execute("DROP DATABASE %s"
|
||||
|
@ -466,16 +463,10 @@ class BaseDatabaseCreation(object):
|
|||
anymore by Django code. Kept for compatibility with user code that
|
||||
might use it.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _prepare_for_test_db_ddl(self):
|
||||
"""
|
||||
Internal implementation - Hook for tasks that should be performed
|
||||
before the ``CREATE DATABASE``/``DROP DATABASE`` clauses used by
|
||||
testing code to create/ destroy test databases. Needed e.g. in
|
||||
PostgreSQL to rollback and close any active transaction.
|
||||
"""
|
||||
pass
|
||||
warnings.warn(
|
||||
"set_autocommit was moved from BaseDatabaseCreation to "
|
||||
"BaseDatabaseWrapper.", PendingDeprecationWarning, stacklevel=2)
|
||||
return self.connection.set_autocommit(True)
|
||||
|
||||
def sql_table_creation_suffix(self):
|
||||
"""
|
||||
|
|
|
@ -55,12 +55,9 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
|||
_savepoint = ignore
|
||||
_savepoint_commit = complain
|
||||
_savepoint_rollback = ignore
|
||||
_enter_transaction_management = complain
|
||||
_leave_transaction_management = ignore
|
||||
_set_autocommit = complain
|
||||
set_dirty = complain
|
||||
set_clean = complain
|
||||
commit_unless_managed = complain
|
||||
rollback_unless_managed = ignore
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DatabaseWrapper, self).__init__(*args, **kwargs)
|
||||
|
|
|
@ -355,15 +355,6 @@ class DatabaseOperations(BaseDatabaseOperations):
|
|||
items_sql = "(%s)" % ", ".join(["%s"] * len(fields))
|
||||
return "VALUES " + ", ".join([items_sql] * num_values)
|
||||
|
||||
def savepoint_create_sql(self, sid):
|
||||
return "SAVEPOINT %s" % sid
|
||||
|
||||
def savepoint_commit_sql(self, sid):
|
||||
return "RELEASE SAVEPOINT %s" % sid
|
||||
|
||||
def savepoint_rollback_sql(self, sid):
|
||||
return "ROLLBACK TO SAVEPOINT %s" % sid
|
||||
|
||||
class DatabaseWrapper(BaseDatabaseWrapper):
|
||||
vendor = 'mysql'
|
||||
operators = {
|
||||
|
@ -445,6 +436,9 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
|||
except Database.NotSupportedError:
|
||||
pass
|
||||
|
||||
def _set_autocommit(self, autocommit):
|
||||
self.connection.autocommit(autocommit)
|
||||
|
||||
def disable_constraint_checking(self):
|
||||
"""
|
||||
Disables foreign key checks, primarily for use in adding rows with forward references. Always returns True,
|
||||
|
|
|
@ -612,6 +612,9 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
|||
def _savepoint_commit(self, sid):
|
||||
pass
|
||||
|
||||
def _set_autocommit(self, autocommit):
|
||||
self.connection.autocommit = autocommit
|
||||
|
||||
def check_constraints(self, table_names=None):
|
||||
"""
|
||||
To check constraints, we set constraints to immediate. Then, when, we're done we must ensure they
|
||||
|
|
|
@ -273,6 +273,3 @@ class DatabaseCreation(BaseDatabaseCreation):
|
|||
settings_dict['NAME'],
|
||||
self._test_database_user(),
|
||||
)
|
||||
|
||||
def set_autocommit(self):
|
||||
self.connection.connection.autocommit = True
|
||||
|
|
|
@ -49,6 +49,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
has_select_for_update = True
|
||||
has_select_for_update_nowait = True
|
||||
has_bulk_insert = True
|
||||
uses_savepoints = True
|
||||
supports_tablespaces = True
|
||||
supports_transactions = True
|
||||
can_distinct_on_fields = True
|
||||
|
@ -77,15 +78,11 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
|||
def __init__(self, *args, **kwargs):
|
||||
super(DatabaseWrapper, self).__init__(*args, **kwargs)
|
||||
|
||||
opts = self.settings_dict["OPTIONS"]
|
||||
RC = psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED
|
||||
self.isolation_level = opts.get('isolation_level', RC)
|
||||
|
||||
self.features = DatabaseFeatures(self)
|
||||
autocommit = self.settings_dict["OPTIONS"].get('autocommit', False)
|
||||
self.features.uses_autocommit = autocommit
|
||||
if autocommit:
|
||||
level = psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT
|
||||
else:
|
||||
level = self.settings_dict["OPTIONS"].get('isolation_level',
|
||||
psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED)
|
||||
self._set_isolation_level(level)
|
||||
self.ops = DatabaseOperations(self)
|
||||
self.client = DatabaseClient(self)
|
||||
self.creation = DatabaseCreation(self)
|
||||
|
@ -135,8 +132,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
|||
|
||||
if conn_tz != tz:
|
||||
# Set the time zone in autocommit mode (see #17062)
|
||||
self.connection.set_isolation_level(
|
||||
psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
|
||||
self.set_autocommit(True)
|
||||
self.connection.cursor().execute(
|
||||
self.ops.set_time_zone_sql(), [tz])
|
||||
self.connection.set_isolation_level(self.isolation_level)
|
||||
|
@ -167,44 +163,22 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
|||
finally:
|
||||
self.set_clean()
|
||||
|
||||
def _enter_transaction_management(self, managed):
|
||||
"""
|
||||
Switch the isolation level when needing transaction support, so that
|
||||
the same transaction is visible across all the queries.
|
||||
"""
|
||||
if self.features.uses_autocommit and managed and not self.isolation_level:
|
||||
level = self.settings_dict["OPTIONS"].get('isolation_level',
|
||||
psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED)
|
||||
self._set_isolation_level(level)
|
||||
def _set_isolation_level(self, isolation_level):
|
||||
assert isolation_level in range(1, 5) # Use set_autocommit for level = 0
|
||||
if self.psycopg2_version >= (2, 4, 2):
|
||||
self.connection.set_session(isolation_level=isolation_level)
|
||||
else:
|
||||
self.connection.set_isolation_level(isolation_level)
|
||||
|
||||
def _leave_transaction_management(self, managed):
|
||||
"""
|
||||
If the normal operating mode is "autocommit", switch back to that when
|
||||
leaving transaction management.
|
||||
"""
|
||||
if self.features.uses_autocommit and not managed and self.isolation_level:
|
||||
self._set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
|
||||
|
||||
def _set_isolation_level(self, level):
|
||||
"""
|
||||
Do all the related feature configurations for changing isolation
|
||||
levels. This doesn't touch the uses_autocommit feature, since that
|
||||
controls the movement *between* isolation levels.
|
||||
"""
|
||||
assert level in range(5)
|
||||
try:
|
||||
if self.connection is not None:
|
||||
self.connection.set_isolation_level(level)
|
||||
if level == psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT:
|
||||
self.set_clean()
|
||||
finally:
|
||||
self.isolation_level = level
|
||||
self.features.uses_savepoints = bool(level)
|
||||
|
||||
def set_dirty(self):
|
||||
if ((self.transaction_state and self.transaction_state[-1]) or
|
||||
not self.features.uses_autocommit):
|
||||
super(DatabaseWrapper, self).set_dirty()
|
||||
def _set_autocommit(self, autocommit):
|
||||
if self.psycopg2_version >= (2, 4, 2):
|
||||
self.connection.autocommit = autocommit
|
||||
else:
|
||||
if autocommit:
|
||||
level = psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT
|
||||
else:
|
||||
level = self.isolation_level
|
||||
self.connection.set_isolation_level(level)
|
||||
|
||||
def check_constraints(self, table_names=None):
|
||||
"""
|
||||
|
@ -223,6 +197,11 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
|||
else:
|
||||
return True
|
||||
|
||||
@cached_property
|
||||
def psycopg2_version(self):
|
||||
version = psycopg2.__version__.split(' ', 1)[0]
|
||||
return tuple(int(v) for v in version.split('.'))
|
||||
|
||||
@cached_property
|
||||
def pg_version(self):
|
||||
with self.temporary_connection():
|
||||
|
|
|
@ -77,14 +77,3 @@ class DatabaseCreation(BaseDatabaseCreation):
|
|||
output.append(get_index_sql('%s_%s_like' % (db_table, f.column),
|
||||
' text_pattern_ops'))
|
||||
return output
|
||||
|
||||
def set_autocommit(self):
|
||||
self._prepare_for_test_db_ddl()
|
||||
|
||||
def _prepare_for_test_db_ddl(self):
|
||||
"""Rollback and close the active transaction."""
|
||||
# Make sure there is an open connection.
|
||||
self.connection.cursor()
|
||||
self.connection.connection.rollback()
|
||||
self.connection.connection.set_isolation_level(
|
||||
psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
|
||||
|
|
|
@ -175,15 +175,6 @@ class DatabaseOperations(BaseDatabaseOperations):
|
|||
style.SQL_TABLE(qn(f.m2m_db_table()))))
|
||||
return output
|
||||
|
||||
def savepoint_create_sql(self, sid):
|
||||
return "SAVEPOINT %s" % sid
|
||||
|
||||
def savepoint_commit_sql(self, sid):
|
||||
return "RELEASE SAVEPOINT %s" % sid
|
||||
|
||||
def savepoint_rollback_sql(self, sid):
|
||||
return "ROLLBACK TO SAVEPOINT %s" % sid
|
||||
|
||||
def prep_for_iexact_query(self, x):
|
||||
return x
|
||||
|
||||
|
|
|
@ -99,6 +99,11 @@ 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):
|
||||
return Database.sqlite_version_info >= (3, 6, 8)
|
||||
|
||||
@cached_property
|
||||
def supports_stddev(self):
|
||||
|
@ -355,6 +360,25 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
|||
if self.settings_dict['NAME'] != ":memory:":
|
||||
BaseDatabaseWrapper.close(self)
|
||||
|
||||
def _savepoint_allowed(self):
|
||||
# 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:
|
||||
level = None
|
||||
else:
|
||||
# sqlite3's internal default is ''. It's different from None.
|
||||
# See Modules/_sqlite/connection.c.
|
||||
level = ''
|
||||
# 'isolation_level' is a misleading API.
|
||||
# SQLite always runs at the SERIALIZABLE isolation level.
|
||||
self.connection.isolation_level = level
|
||||
|
||||
def check_constraints(self, table_names=None):
|
||||
"""
|
||||
Checks each table name in `table_names` for rows with invalid foreign key references. This method is
|
||||
|
@ -392,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')
|
||||
|
||||
|
|
|
@ -72,9 +72,6 @@ class DatabaseCreation(BaseDatabaseCreation):
|
|||
# Remove the SQLite database file
|
||||
os.remove(test_database_name)
|
||||
|
||||
def set_autocommit(self):
|
||||
self.connection.connection.isolation_level = None
|
||||
|
||||
def test_db_signature(self):
|
||||
"""
|
||||
Returns a tuple that uniquely identifies a test database.
|
||||
|
|
|
@ -609,48 +609,48 @@ class Model(six.with_metaclass(ModelBase)):
|
|||
if update_fields:
|
||||
non_pks = [f for f in non_pks if f.name in update_fields or f.attname in update_fields]
|
||||
|
||||
# First, try an UPDATE. If that doesn't update anything, do an INSERT.
|
||||
pk_val = self._get_pk_val(meta)
|
||||
pk_set = pk_val is not None
|
||||
record_exists = True
|
||||
manager = cls._base_manager
|
||||
if pk_set:
|
||||
# Determine if we should do an update (pk already exists, forced update,
|
||||
# no force_insert)
|
||||
if ((force_update or update_fields) or (not force_insert and
|
||||
manager.using(using).filter(pk=pk_val).exists())):
|
||||
if force_update or non_pks:
|
||||
values = [(f, None, (raw and getattr(self, f.attname) or f.pre_save(self, False))) for f in non_pks]
|
||||
if values:
|
||||
rows = manager.using(using).filter(pk=pk_val)._update(values)
|
||||
if force_update and not rows:
|
||||
raise DatabaseError("Forced update did not affect any rows.")
|
||||
if update_fields and not rows:
|
||||
raise DatabaseError("Save with update_fields did not affect any rows.")
|
||||
else:
|
||||
with transaction.commit_on_success_unless_managed(using=using):
|
||||
# First, try an UPDATE. If that doesn't update anything, do an INSERT.
|
||||
pk_val = self._get_pk_val(meta)
|
||||
pk_set = pk_val is not None
|
||||
record_exists = True
|
||||
manager = cls._base_manager
|
||||
if pk_set:
|
||||
# Determine if we should do an update (pk already exists, forced update,
|
||||
# no force_insert)
|
||||
if ((force_update or update_fields) or (not force_insert and
|
||||
manager.using(using).filter(pk=pk_val).exists())):
|
||||
if force_update or non_pks:
|
||||
values = [(f, None, (raw and getattr(self, f.attname) or f.pre_save(self, False))) for f in non_pks]
|
||||
if values:
|
||||
rows = manager.using(using).filter(pk=pk_val)._update(values)
|
||||
if force_update and not rows:
|
||||
raise DatabaseError("Forced update did not affect any rows.")
|
||||
if update_fields and not rows:
|
||||
raise DatabaseError("Save with update_fields did not affect any rows.")
|
||||
else:
|
||||
record_exists = False
|
||||
if not pk_set or not record_exists:
|
||||
if meta.order_with_respect_to:
|
||||
# If this is a model with an order_with_respect_to
|
||||
# autopopulate the _order field
|
||||
field = meta.order_with_respect_to
|
||||
order_value = manager.using(using).filter(**{field.name: getattr(self, field.attname)}).count()
|
||||
self._order = order_value
|
||||
|
||||
fields = meta.local_fields
|
||||
if not pk_set:
|
||||
if force_update or update_fields:
|
||||
raise ValueError("Cannot force an update in save() with no primary key.")
|
||||
fields = [f for f in fields if not isinstance(f, AutoField)]
|
||||
|
||||
record_exists = False
|
||||
if not pk_set or not record_exists:
|
||||
if meta.order_with_respect_to:
|
||||
# If this is a model with an order_with_respect_to
|
||||
# autopopulate the _order field
|
||||
field = meta.order_with_respect_to
|
||||
order_value = manager.using(using).filter(**{field.name: getattr(self, field.attname)}).count()
|
||||
self._order = order_value
|
||||
|
||||
fields = meta.local_fields
|
||||
if not pk_set:
|
||||
if force_update or update_fields:
|
||||
raise ValueError("Cannot force an update in save() with no primary key.")
|
||||
fields = [f for f in fields if not isinstance(f, AutoField)]
|
||||
update_pk = bool(meta.has_auto_field and not pk_set)
|
||||
result = manager._insert([self], fields=fields, return_id=update_pk, using=using, raw=raw)
|
||||
|
||||
record_exists = False
|
||||
|
||||
update_pk = bool(meta.has_auto_field and not pk_set)
|
||||
result = manager._insert([self], fields=fields, return_id=update_pk, using=using, raw=raw)
|
||||
|
||||
if update_pk:
|
||||
setattr(self, meta.pk.attname, result)
|
||||
transaction.commit_unless_managed(using=using)
|
||||
if update_pk:
|
||||
setattr(self, meta.pk.attname, result)
|
||||
|
||||
# Store the database on which the object was saved
|
||||
self._state.db = using
|
||||
|
@ -963,9 +963,9 @@ def method_set_order(ordered_obj, self, id_list, using=None):
|
|||
order_name = ordered_obj._meta.order_with_respect_to.name
|
||||
# FIXME: It would be nice if there was an "update many" version of update
|
||||
# for situations like this.
|
||||
for i, j in enumerate(id_list):
|
||||
ordered_obj.objects.filter(**{'pk': j, order_name: rel_val}).update(_order=i)
|
||||
transaction.commit_unless_managed(using=using)
|
||||
with transaction.commit_on_success_unless_managed(using=using):
|
||||
for i, j in enumerate(id_list):
|
||||
ordered_obj.objects.filter(**{'pk': j, order_name: rel_val}).update(_order=i)
|
||||
|
||||
|
||||
def method_get_order(ordered_obj, self):
|
||||
|
|
|
@ -50,26 +50,6 @@ def DO_NOTHING(collector, field, sub_objs, using):
|
|||
pass
|
||||
|
||||
|
||||
def force_managed(func):
|
||||
@wraps(func)
|
||||
def decorated(self, *args, **kwargs):
|
||||
if not transaction.is_managed(using=self.using):
|
||||
transaction.enter_transaction_management(using=self.using)
|
||||
forced_managed = True
|
||||
else:
|
||||
forced_managed = False
|
||||
try:
|
||||
func(self, *args, **kwargs)
|
||||
if forced_managed:
|
||||
transaction.commit(using=self.using)
|
||||
else:
|
||||
transaction.commit_unless_managed(using=self.using)
|
||||
finally:
|
||||
if forced_managed:
|
||||
transaction.leave_transaction_management(using=self.using)
|
||||
return decorated
|
||||
|
||||
|
||||
class Collector(object):
|
||||
def __init__(self, using):
|
||||
self.using = using
|
||||
|
@ -262,7 +242,6 @@ class Collector(object):
|
|||
self.data = SortedDict([(model, self.data[model])
|
||||
for model in sorted_models])
|
||||
|
||||
@force_managed
|
||||
def delete(self):
|
||||
# sort instance collections
|
||||
for model, instances in self.data.items():
|
||||
|
@ -273,40 +252,41 @@ class Collector(object):
|
|||
# end of a transaction.
|
||||
self.sort()
|
||||
|
||||
# send pre_delete signals
|
||||
for model, obj in self.instances_with_model():
|
||||
if not model._meta.auto_created:
|
||||
signals.pre_delete.send(
|
||||
sender=model, instance=obj, using=self.using
|
||||
)
|
||||
|
||||
# fast deletes
|
||||
for qs in self.fast_deletes:
|
||||
qs._raw_delete(using=self.using)
|
||||
|
||||
# update fields
|
||||
for model, instances_for_fieldvalues in six.iteritems(self.field_updates):
|
||||
query = sql.UpdateQuery(model)
|
||||
for (field, value), instances in six.iteritems(instances_for_fieldvalues):
|
||||
query.update_batch([obj.pk for obj in instances],
|
||||
{field.name: value}, self.using)
|
||||
|
||||
# reverse instance collections
|
||||
for instances in six.itervalues(self.data):
|
||||
instances.reverse()
|
||||
|
||||
# delete instances
|
||||
for model, instances in six.iteritems(self.data):
|
||||
query = sql.DeleteQuery(model)
|
||||
pk_list = [obj.pk for obj in instances]
|
||||
query.delete_batch(pk_list, self.using)
|
||||
|
||||
if not model._meta.auto_created:
|
||||
for obj in instances:
|
||||
signals.post_delete.send(
|
||||
with transaction.commit_on_success_unless_managed(using=self.using):
|
||||
# send pre_delete signals
|
||||
for model, obj in self.instances_with_model():
|
||||
if not model._meta.auto_created:
|
||||
signals.pre_delete.send(
|
||||
sender=model, instance=obj, using=self.using
|
||||
)
|
||||
|
||||
# fast deletes
|
||||
for qs in self.fast_deletes:
|
||||
qs._raw_delete(using=self.using)
|
||||
|
||||
# update fields
|
||||
for model, instances_for_fieldvalues in six.iteritems(self.field_updates):
|
||||
query = sql.UpdateQuery(model)
|
||||
for (field, value), instances in six.iteritems(instances_for_fieldvalues):
|
||||
query.update_batch([obj.pk for obj in instances],
|
||||
{field.name: value}, self.using)
|
||||
|
||||
# reverse instance collections
|
||||
for instances in six.itervalues(self.data):
|
||||
instances.reverse()
|
||||
|
||||
# delete instances
|
||||
for model, instances in six.iteritems(self.data):
|
||||
query = sql.DeleteQuery(model)
|
||||
pk_list = [obj.pk for obj in instances]
|
||||
query.delete_batch(pk_list, self.using)
|
||||
|
||||
if not model._meta.auto_created:
|
||||
for obj in instances:
|
||||
signals.post_delete.send(
|
||||
sender=model, instance=obj, using=self.using
|
||||
)
|
||||
|
||||
# update collected instances
|
||||
for model, instances_for_fieldvalues in six.iteritems(self.field_updates):
|
||||
for (field, value), instances in six.iteritems(instances_for_fieldvalues):
|
||||
|
|
|
@ -442,12 +442,7 @@ class QuerySet(object):
|
|||
self._for_write = True
|
||||
connection = connections[self.db]
|
||||
fields = self.model._meta.local_fields
|
||||
if not transaction.is_managed(using=self.db):
|
||||
transaction.enter_transaction_management(using=self.db)
|
||||
forced_managed = True
|
||||
else:
|
||||
forced_managed = False
|
||||
try:
|
||||
with transaction.commit_on_success_unless_managed(using=self.db):
|
||||
if (connection.features.can_combine_inserts_with_and_without_auto_increment_pk
|
||||
and self.model._meta.has_auto_field):
|
||||
self._batched_insert(objs, fields, batch_size)
|
||||
|
@ -458,13 +453,6 @@ class QuerySet(object):
|
|||
if objs_without_pk:
|
||||
fields= [f for f in fields if not isinstance(f, AutoField)]
|
||||
self._batched_insert(objs_without_pk, fields, batch_size)
|
||||
if forced_managed:
|
||||
transaction.commit(using=self.db)
|
||||
else:
|
||||
transaction.commit_unless_managed(using=self.db)
|
||||
finally:
|
||||
if forced_managed:
|
||||
transaction.leave_transaction_management(using=self.db)
|
||||
|
||||
return objs
|
||||
|
||||
|
@ -581,20 +569,8 @@ class QuerySet(object):
|
|||
self._for_write = True
|
||||
query = self.query.clone(sql.UpdateQuery)
|
||||
query.add_update_values(kwargs)
|
||||
if not transaction.is_managed(using=self.db):
|
||||
transaction.enter_transaction_management(using=self.db)
|
||||
forced_managed = True
|
||||
else:
|
||||
forced_managed = False
|
||||
try:
|
||||
with transaction.commit_on_success_unless_managed(using=self.db):
|
||||
rows = query.get_compiler(self.db).execute_sql(None)
|
||||
if forced_managed:
|
||||
transaction.commit(using=self.db)
|
||||
else:
|
||||
transaction.commit_unless_managed(using=self.db)
|
||||
finally:
|
||||
if forced_managed:
|
||||
transaction.leave_transaction_management(using=self.db)
|
||||
self._result_cache = None
|
||||
return rows
|
||||
update.alters_data = True
|
||||
|
|
|
@ -12,9 +12,11 @@ Managed transactions don't do those commits, but will need some kind of manual
|
|||
or implicit commits or rollbacks.
|
||||
"""
|
||||
|
||||
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):
|
||||
|
@ -37,6 +39,10 @@ def get_connection(using=None):
|
|||
using = DEFAULT_DB_ALIAS
|
||||
return connections[using]
|
||||
|
||||
###########################
|
||||
# Deprecated private APIs #
|
||||
###########################
|
||||
|
||||
def abort(using=None):
|
||||
"""
|
||||
Roll back any ongoing transactions and clean the transaction management
|
||||
|
@ -49,7 +55,7 @@ def abort(using=None):
|
|||
"""
|
||||
get_connection(using).abort()
|
||||
|
||||
def enter_transaction_management(managed=True, using=None):
|
||||
def enter_transaction_management(managed=True, using=None, forced=False):
|
||||
"""
|
||||
Enters transaction management for a running thread. It must be balanced with
|
||||
the appropriate leave_transaction_management call, since the actual state is
|
||||
|
@ -59,7 +65,7 @@ def enter_transaction_management(managed=True, using=None):
|
|||
from the settings, if there is no surrounding block (dirty is always false
|
||||
when no current block is running).
|
||||
"""
|
||||
get_connection(using).enter_transaction_management(managed)
|
||||
get_connection(using).enter_transaction_management(managed, forced)
|
||||
|
||||
def leave_transaction_management(using=None):
|
||||
"""
|
||||
|
@ -92,52 +98,47 @@ def set_clean(using=None):
|
|||
"""
|
||||
get_connection(using).set_clean()
|
||||
|
||||
def clean_savepoints(using=None):
|
||||
"""
|
||||
Resets the counter used to generate unique savepoint ids in this thread.
|
||||
"""
|
||||
get_connection(using).clean_savepoints()
|
||||
|
||||
def is_managed(using=None):
|
||||
"""
|
||||
Checks whether the transaction manager is in manual or in auto state.
|
||||
"""
|
||||
return get_connection(using).is_managed()
|
||||
warnings.warn("'is_managed' is deprecated.",
|
||||
PendingDeprecationWarning, stacklevel=2)
|
||||
|
||||
def managed(flag=True, using=None):
|
||||
"""
|
||||
Puts the transaction manager into a manual state: managed transactions have
|
||||
to be committed explicitly by the user. If you switch off transaction
|
||||
management and there is a pending commit/rollback, the data will be
|
||||
commited.
|
||||
"""
|
||||
get_connection(using).managed(flag)
|
||||
warnings.warn("'managed' no longer serves a purpose.",
|
||||
PendingDeprecationWarning, stacklevel=2)
|
||||
|
||||
def commit_unless_managed(using=None):
|
||||
"""
|
||||
Commits changes if the system is not in managed transaction mode.
|
||||
"""
|
||||
get_connection(using).commit_unless_managed()
|
||||
warnings.warn("'commit_unless_managed' is now a no-op.",
|
||||
PendingDeprecationWarning, stacklevel=2)
|
||||
|
||||
def rollback_unless_managed(using=None):
|
||||
"""
|
||||
Rolls back changes if the system is not in managed transaction mode.
|
||||
"""
|
||||
get_connection(using).rollback_unless_managed()
|
||||
warnings.warn("'rollback_unless_managed' is now a no-op.",
|
||||
PendingDeprecationWarning, stacklevel=2)
|
||||
|
||||
###############
|
||||
# Public APIs #
|
||||
###############
|
||||
|
||||
def get_autocommit(using=None):
|
||||
"""
|
||||
Get the autocommit status of the connection.
|
||||
"""
|
||||
return get_connection(using).autocommit
|
||||
|
||||
def set_autocommit(autocommit, using=None):
|
||||
"""
|
||||
Set the autocommit status of the connection.
|
||||
"""
|
||||
return get_connection(using).set_autocommit(autocommit)
|
||||
|
||||
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()
|
||||
|
||||
|
@ -163,9 +164,193 @@ def savepoint_commit(sid, using=None):
|
|||
"""
|
||||
get_connection(using).savepoint_commit(sid)
|
||||
|
||||
##############
|
||||
# DECORATORS #
|
||||
##############
|
||||
def clean_savepoints(using=None):
|
||||
"""
|
||||
Resets the counter used to generate unique savepoint ids in this thread.
|
||||
"""
|
||||
get_connection(using).clean_savepoints()
|
||||
|
||||
#################################
|
||||
# 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.
|
||||
|
||||
It's possible to disable the creation of savepoints if the goal is to
|
||||
ensure that some code runs within a transaction without creating overhead.
|
||||
|
||||
A stack of savepoints identifiers is maintained as an attribute of the
|
||||
connection. None denotes the absence of a savepoint.
|
||||
|
||||
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, savepoint):
|
||||
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)
|
||||
|
||||
# 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, unless we
|
||||
# were told not to or we're already waiting for a rollback. The
|
||||
# second condition avoids creating useless savepoints and prevents
|
||||
# overwriting needs_rollback until the rollback is performed.
|
||||
if self.savepoint and not connection.needs_rollback:
|
||||
sid = connection.savepoint()
|
||||
connection.savepoint_ids.append(sid)
|
||||
else:
|
||||
connection.savepoint_ids.append(None)
|
||||
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.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:
|
||||
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:
|
||||
# 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:
|
||||
# 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
|
||||
else:
|
||||
connection.savepoint_rollback(sid)
|
||||
else:
|
||||
# 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)
|
||||
|
||||
# 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, savepoint=True):
|
||||
# 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, savepoint)(using)
|
||||
# Decorator: @atomic(...) or context manager: with atomic(...): ...
|
||||
else:
|
||||
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 #
|
||||
############################################
|
||||
|
||||
class Transaction(object):
|
||||
"""
|
||||
|
@ -222,9 +407,11 @@ def autocommit(using=None):
|
|||
this decorator is useful if you globally activated transaction management in
|
||||
your settings file and want the default behavior in some view functions.
|
||||
"""
|
||||
warnings.warn("autocommit is deprecated in favor of set_autocommit.",
|
||||
PendingDeprecationWarning, stacklevel=2)
|
||||
|
||||
def entering(using):
|
||||
enter_transaction_management(managed=False, using=using)
|
||||
managed(False, using=using)
|
||||
|
||||
def exiting(exc_value, using):
|
||||
leave_transaction_management(using=using)
|
||||
|
@ -238,9 +425,11 @@ def commit_on_success(using=None):
|
|||
a rollback is made. This is one of the most common ways to do transaction
|
||||
control in Web apps.
|
||||
"""
|
||||
warnings.warn("commit_on_success is deprecated in favor of atomic.",
|
||||
PendingDeprecationWarning, stacklevel=2)
|
||||
|
||||
def entering(using):
|
||||
enter_transaction_management(using=using)
|
||||
managed(True, using=using)
|
||||
|
||||
def exiting(exc_value, using):
|
||||
try:
|
||||
|
@ -266,11 +455,37 @@ def commit_manually(using=None):
|
|||
own -- it's up to the user to call the commit and rollback functions
|
||||
themselves.
|
||||
"""
|
||||
warnings.warn("commit_manually is deprecated in favor of set_autocommit.",
|
||||
PendingDeprecationWarning, stacklevel=2)
|
||||
|
||||
def entering(using):
|
||||
enter_transaction_management(using=using)
|
||||
managed(True, using=using)
|
||||
|
||||
def exiting(exc_value, using):
|
||||
leave_transaction_management(using=using)
|
||||
|
||||
return _transaction_func(entering, exiting, using)
|
||||
|
||||
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.
|
||||
|
||||
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)
|
||||
else:
|
||||
def entering(using):
|
||||
pass
|
||||
|
||||
def exiting(exc_value, using):
|
||||
set_dirty(using=using)
|
||||
|
||||
return _transaction_func(entering, exiting, using)
|
||||
|
|
|
@ -2,6 +2,7 @@ from functools import wraps
|
|||
import os
|
||||
import pkgutil
|
||||
from threading import local
|
||||
import warnings
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
@ -158,6 +159,13 @@ class ConnectionHandler(object):
|
|||
except KeyError:
|
||||
raise ConnectionDoesNotExist("The connection %s doesn't exist" % alias)
|
||||
|
||||
conn.setdefault('ATOMIC_REQUESTS', False)
|
||||
if settings.TRANSACTIONS_MANAGED:
|
||||
warnings.warn(
|
||||
"TRANSACTIONS_MANAGED is deprecated. Use AUTOCOMMIT instead.",
|
||||
PendingDeprecationWarning, stacklevel=2)
|
||||
conn.setdefault('AUTOCOMMIT', False)
|
||||
conn.setdefault('AUTOCOMMIT', True)
|
||||
conn.setdefault('ENGINE', 'django.db.backends.dummy')
|
||||
if conn['ENGINE'] == 'django.db.backends.' or not conn['ENGINE']:
|
||||
conn['ENGINE'] = 'django.db.backends.dummy'
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
from django.db import transaction
|
||||
import warnings
|
||||
|
||||
from django.core.exceptions import MiddlewareNotUsed
|
||||
from django.db import connection, transaction
|
||||
|
||||
class TransactionMiddleware(object):
|
||||
"""
|
||||
|
@ -7,10 +10,17 @@ class TransactionMiddleware(object):
|
|||
commit, the commit is done when a successful response is created. If an
|
||||
exception happens, the database is rolled back.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
warnings.warn(
|
||||
"TransactionMiddleware is deprecated in favor of ATOMIC_REQUESTS.",
|
||||
PendingDeprecationWarning, stacklevel=2)
|
||||
if connection.settings_dict['ATOMIC_REQUESTS']:
|
||||
raise MiddlewareNotUsed
|
||||
|
||||
def process_request(self, request):
|
||||
"""Enters transaction management"""
|
||||
transaction.enter_transaction_management()
|
||||
transaction.managed(True)
|
||||
|
||||
def process_exception(self, request, exception):
|
||||
"""Rolls back the database and leaves transaction management"""
|
||||
|
@ -24,7 +34,7 @@ class TransactionMiddleware(object):
|
|||
|
||||
def process_response(self, request, response):
|
||||
"""Commits and leaves transaction management."""
|
||||
if transaction.is_managed():
|
||||
if not transaction.get_autocommit():
|
||||
if transaction.is_dirty():
|
||||
# Note: it is possible that the commit fails. If the reason is
|
||||
# closed connection or some similar reason, then there is
|
||||
|
|
|
@ -67,7 +67,6 @@ real_commit = transaction.commit
|
|||
real_rollback = transaction.rollback
|
||||
real_enter_transaction_management = transaction.enter_transaction_management
|
||||
real_leave_transaction_management = transaction.leave_transaction_management
|
||||
real_managed = transaction.managed
|
||||
real_abort = transaction.abort
|
||||
|
||||
def nop(*args, **kwargs):
|
||||
|
@ -78,7 +77,6 @@ def disable_transaction_methods():
|
|||
transaction.rollback = nop
|
||||
transaction.enter_transaction_management = nop
|
||||
transaction.leave_transaction_management = nop
|
||||
transaction.managed = nop
|
||||
transaction.abort = nop
|
||||
|
||||
def restore_transaction_methods():
|
||||
|
@ -86,7 +84,6 @@ def restore_transaction_methods():
|
|||
transaction.rollback = real_rollback
|
||||
transaction.enter_transaction_management = real_enter_transaction_management
|
||||
transaction.leave_transaction_management = real_leave_transaction_management
|
||||
transaction.managed = real_managed
|
||||
transaction.abort = real_abort
|
||||
|
||||
|
||||
|
@ -157,14 +154,6 @@ class DocTestRunner(doctest.DocTestRunner):
|
|||
doctest.DocTestRunner.__init__(self, *args, **kwargs)
|
||||
self.optionflags = doctest.ELLIPSIS
|
||||
|
||||
def report_unexpected_exception(self, out, test, example, exc_info):
|
||||
doctest.DocTestRunner.report_unexpected_exception(self, out, test,
|
||||
example, exc_info)
|
||||
# Rollback, in case of database errors. Otherwise they'd have
|
||||
# side effects on other tests.
|
||||
for conn in connections:
|
||||
transaction.rollback_unless_managed(using=conn)
|
||||
|
||||
|
||||
class _AssertNumQueriesContext(CaptureQueriesContext):
|
||||
def __init__(self, test_case, num, connection):
|
||||
|
@ -490,14 +479,10 @@ class TransactionTestCase(SimpleTestCase):
|
|||
conn.ops.sequence_reset_by_name_sql(no_style(),
|
||||
conn.introspection.sequence_list())
|
||||
if sql_list:
|
||||
try:
|
||||
with transaction.commit_on_success_unless_managed(using=db_name):
|
||||
cursor = conn.cursor()
|
||||
for sql in sql_list:
|
||||
cursor.execute(sql)
|
||||
except Exception:
|
||||
transaction.rollback_unless_managed(using=db_name)
|
||||
raise
|
||||
transaction.commit_unless_managed(using=db_name)
|
||||
|
||||
def _fixture_setup(self):
|
||||
for db_name in self._databases_names(include_mirrors=False):
|
||||
|
@ -537,11 +522,6 @@ class TransactionTestCase(SimpleTestCase):
|
|||
conn.close()
|
||||
|
||||
def _fixture_teardown(self):
|
||||
# Roll back any pending transactions in order to avoid a deadlock
|
||||
# during flush when TEST_MIRROR is used (#18984).
|
||||
for conn in connections.all():
|
||||
conn.rollback_unless_managed()
|
||||
|
||||
for db in self._databases_names(include_mirrors=False):
|
||||
call_command('flush', verbosity=0, interactive=False, database=db,
|
||||
skip_validation=True, reset_sequences=False)
|
||||
|
@ -831,9 +811,11 @@ class TestCase(TransactionTestCase):
|
|||
|
||||
assert not self.reset_sequences, 'reset_sequences cannot be used on TestCase instances'
|
||||
|
||||
self.atomics = {}
|
||||
for db_name in self._databases_names():
|
||||
transaction.enter_transaction_management(using=db_name)
|
||||
transaction.managed(True, using=db_name)
|
||||
self.atomics[db_name] = transaction.atomic(using=db_name)
|
||||
self.atomics[db_name].__enter__()
|
||||
# Remove this when the legacy transaction management goes away.
|
||||
disable_transaction_methods()
|
||||
|
||||
from django.contrib.sites.models import Site
|
||||
|
@ -853,10 +835,12 @@ class TestCase(TransactionTestCase):
|
|||
if not connections_support_transactions():
|
||||
return super(TestCase, self)._fixture_teardown()
|
||||
|
||||
# Remove this when the legacy transaction management goes away.
|
||||
restore_transaction_methods()
|
||||
for db in self._databases_names():
|
||||
transaction.rollback(using=db)
|
||||
transaction.leave_transaction_management(using=db)
|
||||
for db_name in reversed(self._databases_names()):
|
||||
# Hack to force a rollback
|
||||
connections[db_name].needs_rollback = True
|
||||
self.atomics[db_name].__exit__(None, None, None)
|
||||
|
||||
|
||||
def _deferredSkip(condition, reason):
|
||||
|
|
|
@ -329,6 +329,15 @@ these changes.
|
|||
1.8
|
||||
---
|
||||
|
||||
* The following transaction management APIs will be removed:
|
||||
|
||||
- ``TransactionMiddleware``,
|
||||
- the decorators and context managers ``autocommit``, ``commit_on_success``,
|
||||
and ``commit_manually``,
|
||||
- the ``TRANSACTIONS_MANAGED`` setting.
|
||||
|
||||
Upgrade paths are described in :ref:`transactions-upgrading-from-1.5`.
|
||||
|
||||
* The :ttag:`cycle` and :ttag:`firstof` template tags will auto-escape their
|
||||
arguments. In 1.6 and 1.7, this behavior is provided by the version of these
|
||||
tags in the ``future`` template tag library.
|
||||
|
@ -339,8 +348,6 @@ these changes.
|
|||
|
||||
* ``Model._meta.module_name`` was renamed to ``model_name``.
|
||||
|
||||
* The private API ``django.db.close_connection`` will be removed.
|
||||
|
||||
* Remove the backward compatible shims introduced to rename ``get_query_set``
|
||||
and similar queryset methods. This affects the following classes:
|
||||
``BaseModelAdmin``, ``ChangeList``, ``BaseCommentNode``,
|
||||
|
@ -350,6 +357,14 @@ these changes.
|
|||
* Remove the backward compatible shims introduced to rename the attributes
|
||||
``ChangeList.root_query_set`` and ``ChangeList.query_set``.
|
||||
|
||||
* The following private APIs will be removed:
|
||||
- ``django.db.close_connection()``
|
||||
- ``django.db.backends.creation.BaseDatabaseCreation.set_autocommit()``
|
||||
- ``django.db.transaction.is_managed()``
|
||||
- ``django.db.transaction.managed()``
|
||||
- ``django.db.transaction.commit_unless_managed()``
|
||||
- ``django.db.transaction.rollback_unless_managed()``
|
||||
|
||||
2.0
|
||||
---
|
||||
|
||||
|
|
|
@ -69,7 +69,6 @@ even ``0``, because it doesn't make sense to maintain a connection that's
|
|||
unlikely to be reused. This will help keep the number of simultaneous
|
||||
connections to this database small.
|
||||
|
||||
|
||||
The development server creates a new thread for each request it handles,
|
||||
negating the effect of persistent connections.
|
||||
|
||||
|
@ -104,7 +103,8 @@ Optimizing PostgreSQL's configuration
|
|||
Django needs the following parameters for its database connections:
|
||||
|
||||
- ``client_encoding``: ``'UTF8'``,
|
||||
- ``default_transaction_isolation``: ``'read committed'``,
|
||||
- ``default_transaction_isolation``: ``'read committed'`` by default,
|
||||
or the value set in the connection options (see below),
|
||||
- ``timezone``: ``'UTC'`` when :setting:`USE_TZ` is ``True``, value of
|
||||
:setting:`TIME_ZONE` otherwise.
|
||||
|
||||
|
@ -118,30 +118,16 @@ will do some additional queries to set these parameters.
|
|||
|
||||
.. _ALTER ROLE: http://www.postgresql.org/docs/current/interactive/sql-alterrole.html
|
||||
|
||||
Transaction handling
|
||||
---------------------
|
||||
|
||||
:doc:`By default </topics/db/transactions>`, Django runs with an open
|
||||
transaction which it commits automatically when any built-in, data-altering
|
||||
model function is called. The PostgreSQL backends normally operate the same as
|
||||
any other Django backend in this respect.
|
||||
|
||||
.. _postgresql-autocommit-mode:
|
||||
|
||||
Autocommit mode
|
||||
~~~~~~~~~~~~~~~
|
||||
---------------
|
||||
|
||||
If your application is particularly read-heavy and doesn't make many
|
||||
database writes, the overhead of a constantly open transaction can
|
||||
sometimes be noticeable. For those situations, you can configure Django
|
||||
to use *"autocommit"* behavior for the connection, meaning that each database
|
||||
operation will normally be in its own transaction, rather than having
|
||||
the transaction extend over multiple operations. In this case, you can
|
||||
still manually start a transaction if you're doing something that
|
||||
requires consistency across multiple database operations. The
|
||||
autocommit behavior is enabled by setting the ``autocommit`` key in
|
||||
the :setting:`OPTIONS` part of your database configuration in
|
||||
:setting:`DATABASES`::
|
||||
.. versionchanged:: 1.6
|
||||
|
||||
In previous versions of Django, database-level autocommit could be enabled by
|
||||
setting the ``autocommit`` key in the :setting:`OPTIONS` part of your database
|
||||
configuration in :setting:`DATABASES`::
|
||||
|
||||
DATABASES = {
|
||||
# ...
|
||||
|
@ -150,29 +136,11 @@ the :setting:`OPTIONS` part of your database configuration in
|
|||
},
|
||||
}
|
||||
|
||||
In this configuration, Django still ensures that :ref:`delete()
|
||||
<topics-db-queries-delete>` and :ref:`update() <topics-db-queries-update>`
|
||||
queries run inside a single transaction, so that either all the affected
|
||||
objects are changed or none of them are.
|
||||
|
||||
.. admonition:: This is database-level autocommit
|
||||
|
||||
This functionality is not the same as the :ref:`autocommit
|
||||
<topics-db-transactions-autocommit>` decorator. That decorator is
|
||||
a Django-level implementation that commits automatically after
|
||||
data changing operations. The feature enabled using the
|
||||
:setting:`OPTIONS` option provides autocommit behavior at the
|
||||
database adapter level. It commits after *every* operation.
|
||||
|
||||
If you are using this feature and performing an operation akin to delete or
|
||||
updating that requires multiple operations, you are strongly recommended to
|
||||
wrap you operations in manual transaction handling to ensure data consistency.
|
||||
You should also audit your existing code for any instances of this behavior
|
||||
before enabling this feature. It's faster, but it provides less automatic
|
||||
protection for multi-call operations.
|
||||
Since Django 1.6, autocommit is turned on by default. This configuration is
|
||||
ignored and can be safely removed.
|
||||
|
||||
Isolation level
|
||||
~~~~~~~~~~~~~~~
|
||||
---------------
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
|
@ -200,7 +168,7 @@ such as ``REPEATABLE READ`` or ``SERIALIZABLE``, set it in the
|
|||
.. _postgresql-isolation-levels: http://www.postgresql.org/docs/devel/static/transaction-iso.html
|
||||
|
||||
Indexes for ``varchar`` and ``text`` columns
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
--------------------------------------------
|
||||
|
||||
When specifying ``db_index=True`` on your model fields, Django typically
|
||||
outputs a single ``CREATE INDEX`` statement. However, if the database type
|
||||
|
@ -456,8 +424,7 @@ Savepoints
|
|||
|
||||
Both the Django ORM and MySQL (when using the InnoDB :ref:`storage engine
|
||||
<mysql-storage-engines>`) support database :ref:`savepoints
|
||||
<topics-db-transactions-savepoints>`, but this feature wasn't available in
|
||||
Django until version 1.4 when such supports was added.
|
||||
<topics-db-transactions-savepoints>`.
|
||||
|
||||
If you use the MyISAM storage engine please be aware of the fact that you will
|
||||
receive database-generated errors if you try to use the :ref:`savepoint-related
|
||||
|
|
|
@ -205,6 +205,10 @@ Transaction middleware
|
|||
|
||||
.. class:: TransactionMiddleware
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
``TransactionMiddleware`` is deprecated. The documentation of transactions
|
||||
contains :ref:`upgrade instructions <transactions-upgrading-from-1.5>`.
|
||||
|
||||
Binds commit and rollback of the default database to the request/response
|
||||
phase. If a view function runs successfully, a commit is done. If it fails with
|
||||
an exception, a rollback is done.
|
||||
|
|
|
@ -814,8 +814,8 @@ generating large CSV files.
|
|||
.. admonition:: Performance considerations
|
||||
|
||||
Django is designed for short-lived requests. Streaming responses will tie
|
||||
a worker process and keep a database connection idle in transaction for
|
||||
the entire duration of the response. This may result in poor performance.
|
||||
a worker process for the entire duration of the response. This may result
|
||||
in poor performance.
|
||||
|
||||
Generally speaking, you should perform expensive tasks outside of the
|
||||
request-response cycle, rather than resorting to a streamed response.
|
||||
|
|
|
@ -408,6 +408,30 @@ SQLite. This can be configured using the following::
|
|||
For other database backends, or more complex SQLite configurations, other options
|
||||
will be required. The following inner options are available.
|
||||
|
||||
.. setting:: DATABASE-ATOMIC_REQUESTS
|
||||
|
||||
ATOMIC_REQUESTS
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
Default: ``False``
|
||||
|
||||
Set this to ``True`` to wrap each HTTP request in a transaction on this
|
||||
database. See :ref:`tying-transactions-to-http-requests`.
|
||||
|
||||
.. setting:: DATABASE-AUTOCOMMIT
|
||||
|
||||
AUTOCOMMIT
|
||||
~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
Default: ``True``
|
||||
|
||||
Set this to ``False`` if you want to :ref:`disable Django's transaction
|
||||
management <deactivate-transaction-management>` and implement your own.
|
||||
|
||||
.. setting:: DATABASE-ENGINE
|
||||
|
||||
ENGINE
|
||||
|
@ -1807,6 +1831,12 @@ to ensure your processes are running in the correct environment.
|
|||
TRANSACTIONS_MANAGED
|
||||
--------------------
|
||||
|
||||
.. deprecated:: 1.6
|
||||
|
||||
This setting was deprecated because its name is very misleading. Use the
|
||||
:setting:`AUTOCOMMIT <DATABASE-AUTOCOMMIT>` key in :setting:`DATABASES`
|
||||
entries instead.
|
||||
|
||||
Default: ``False``
|
||||
|
||||
Set this to ``True`` if you want to :ref:`disable Django's transaction
|
||||
|
|
|
@ -105,16 +105,14 @@ you just won't get any of the nice new unittest2 features.
|
|||
Transaction context managers
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Users of Python 2.5 and above may now use :ref:`transaction management functions
|
||||
<transaction-management-functions>` as `context managers`_. For example::
|
||||
Users of Python 2.5 and above may now use transaction management functions as
|
||||
`context managers`_. For example::
|
||||
|
||||
with transaction.autocommit():
|
||||
# ...
|
||||
|
||||
.. _context managers: http://docs.python.org/glossary.html#term-context-manager
|
||||
|
||||
For more information, see :ref:`transaction-management-functions`.
|
||||
|
||||
Configurable delete-cascade
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -148,16 +148,14 @@ you just won't get any of the nice new unittest2 features.
|
|||
Transaction context managers
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Users of Python 2.5 and above may now use :ref:`transaction management functions
|
||||
<transaction-management-functions>` as `context managers`_. For example::
|
||||
Users of Python 2.5 and above may now use transaction management functions as
|
||||
`context managers`_. For example::
|
||||
|
||||
with transaction.autocommit():
|
||||
# ...
|
||||
|
||||
.. _context managers: http://docs.python.org/glossary.html#term-context-manager
|
||||
|
||||
For more information, see :ref:`transaction-management-functions`.
|
||||
|
||||
Configurable delete-cascade
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -30,6 +30,18 @@ prevention <clickjacking-prevention>` are turned on.
|
|||
If the default templates don't suit your tastes, you can use :ref:`custom
|
||||
project and app templates <custom-app-and-project-templates>`.
|
||||
|
||||
Improved transaction management
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Django's transaction management was overhauled. Database-level autocommit is
|
||||
now turned on by default. This makes transaction handling more explicit and
|
||||
should improve performance. The existing APIs were deprecated, and new APIs
|
||||
were introduced, as described in :doc:`/topics/db/transactions`.
|
||||
|
||||
Please review carefully the list of :ref:`known backwards-incompatibilities
|
||||
<transactions-upgrading-from-1.5>` to determine if you need to make changes in
|
||||
your code.
|
||||
|
||||
Persistent database connections
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -148,6 +160,16 @@ Backwards incompatible changes in 1.6
|
|||
deprecation timeline for a given feature, its removal may appear as a
|
||||
backwards incompatible change.
|
||||
|
||||
* Database-level autocommit is enabled by default in Django 1.6. While this
|
||||
doesn't change the general spirit of Django's transaction management, there
|
||||
are a few known backwards-incompatibities, described in the :ref:`transaction
|
||||
management docs <transactions-upgrading-from-1.5>`. You should review your code
|
||||
to determine if you're affected.
|
||||
|
||||
* In previous versions, database-level autocommit was only an option for
|
||||
PostgreSQL, and it was disabled by default. This option is now
|
||||
:ref:`ignored <postgresql-autocommit-mode>`.
|
||||
|
||||
* The ``django.db.models.query.EmptyQuerySet`` can't be instantiated any more -
|
||||
it is only usable as a marker class for checking if
|
||||
:meth:`~django.db.models.query.QuerySet.none` has been called:
|
||||
|
@ -234,6 +256,21 @@ Backwards incompatible changes in 1.6
|
|||
Features deprecated in 1.6
|
||||
==========================
|
||||
|
||||
Transaction management APIs
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Transaction management was completely overhauled in Django 1.6, and the
|
||||
current APIs are deprecated:
|
||||
|
||||
- ``django.middleware.transaction.TransactionMiddleware``
|
||||
- ``django.db.transaction.autocommit``
|
||||
- ``django.db.transaction.commit_on_success``
|
||||
- ``django.db.transaction.commit_manually``
|
||||
- the ``TRANSACTIONS_MANAGED`` setting
|
||||
|
||||
The reasons for this change and the upgrade path are described in the
|
||||
:ref:`transactions documentation <transactions-upgrading-from-1.5>`.
|
||||
|
||||
Changes to :ttag:`cycle` and :ttag:`firstof`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -201,31 +201,32 @@ perform queries that don't map cleanly to models, or directly execute
|
|||
In these cases, you can always access the database directly, routing around
|
||||
the model layer entirely.
|
||||
|
||||
The object ``django.db.connection`` represents the
|
||||
default database connection, and ``django.db.transaction`` represents the
|
||||
default database transaction. To use the database connection, call
|
||||
``connection.cursor()`` to get a cursor object. Then, call
|
||||
``cursor.execute(sql, [params])`` to execute the SQL and ``cursor.fetchone()``
|
||||
or ``cursor.fetchall()`` to return the resulting rows. After performing a data
|
||||
changing operation, you should then call
|
||||
``transaction.commit_unless_managed()`` to ensure your changes are committed
|
||||
to the database. If your query is purely a data retrieval operation, no commit
|
||||
is required. For example::
|
||||
The object ``django.db.connection`` represents the default database
|
||||
connection. To use the database connection, call ``connection.cursor()`` to
|
||||
get a cursor object. Then, call ``cursor.execute(sql, [params])`` to execute
|
||||
the SQL and ``cursor.fetchone()`` or ``cursor.fetchall()`` to return the
|
||||
resulting rows.
|
||||
|
||||
For example::
|
||||
|
||||
from django.db import connection
|
||||
|
||||
def my_custom_sql():
|
||||
from django.db import connection, transaction
|
||||
cursor = connection.cursor()
|
||||
|
||||
# Data modifying operation - commit required
|
||||
cursor.execute("UPDATE bar SET foo = 1 WHERE baz = %s", [self.baz])
|
||||
transaction.commit_unless_managed()
|
||||
|
||||
# Data retrieval operation - no commit required
|
||||
cursor.execute("SELECT foo FROM bar WHERE baz = %s", [self.baz])
|
||||
row = cursor.fetchone()
|
||||
|
||||
return row
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
In Django 1.5 and earlier, after performing a data changing operation, you
|
||||
had to call ``transaction.commit_unless_managed()`` to ensure your changes
|
||||
were committed to the database. Since Django now defaults to database-level
|
||||
autocommit, this isn't necessary any longer.
|
||||
|
||||
If you are using :doc:`more than one database </topics/db/multi-db>`, you can
|
||||
use ``django.db.connections`` to obtain the connection (and cursor) for a
|
||||
specific database. ``django.db.connections`` is a dictionary-like
|
||||
|
@ -235,7 +236,6 @@ alias::
|
|||
from django.db import connections
|
||||
cursor = connections['my_db_alias'].cursor()
|
||||
# Your code here...
|
||||
transaction.commit_unless_managed(using='my_db_alias')
|
||||
|
||||
By default, the Python DB API will return results without their field
|
||||
names, which means you end up with a ``list`` of values, rather than a
|
||||
|
@ -260,27 +260,18 @@ Here is an example of the difference between the two::
|
|||
>>> dictfetchall(cursor)
|
||||
[{'parent_id': None, 'id': 54360982L}, {'parent_id': None, 'id': 54360880L}]
|
||||
|
||||
|
||||
.. _transactions-and-raw-sql:
|
||||
|
||||
Transactions and raw SQL
|
||||
------------------------
|
||||
|
||||
When you make a raw SQL call, Django will automatically mark the
|
||||
current transaction as dirty. You must then ensure that the
|
||||
transaction containing those calls is closed correctly. See :ref:`the
|
||||
notes on the requirements of Django's transaction handling
|
||||
<topics-db-transactions-requirements>` for more details.
|
||||
|
||||
Connections and cursors
|
||||
-----------------------
|
||||
|
||||
``connection`` and ``cursor`` mostly implement the standard Python DB-API
|
||||
described in :pep:`249` (except when it comes to :doc:`transaction handling
|
||||
</topics/db/transactions>`). If you're not familiar with the Python DB-API, note
|
||||
that the SQL statement in ``cursor.execute()`` uses placeholders, ``"%s"``,
|
||||
rather than adding parameters directly within the SQL. If you use this
|
||||
technique, the underlying database library will automatically add quotes and
|
||||
escaping to your parameter(s) as necessary. (Also note that Django expects the
|
||||
``"%s"`` placeholder, *not* the ``"?"`` placeholder, which is used by the SQLite
|
||||
Python bindings. This is for the sake of consistency and sanity.)
|
||||
described in :pep:`249` — except when it comes to :doc:`transaction handling
|
||||
</topics/db/transactions>`.
|
||||
|
||||
If you're not familiar with the Python DB-API, note that the SQL statement in
|
||||
``cursor.execute()`` uses placeholders, ``"%s"``, rather than adding
|
||||
parameters directly within the SQL. If you use this technique, the underlying
|
||||
database library will automatically escape your parameters as necessary.
|
||||
|
||||
Also note that Django expects the ``"%s"`` placeholder, *not* the ``"?"``
|
||||
placeholder, which is used by the SQLite Python bindings. This is for the sake
|
||||
of consistency and sanity.
|
||||
|
|
|
@ -1,286 +1,375 @@
|
|||
==============================
|
||||
Managing database transactions
|
||||
==============================
|
||||
=====================
|
||||
Database transactions
|
||||
=====================
|
||||
|
||||
.. module:: django.db.transaction
|
||||
|
||||
Django gives you a few ways to control how database transactions are managed,
|
||||
if you're using a database that supports transactions.
|
||||
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 with an open transaction which it
|
||||
commits automatically when any built-in, data-altering model function is
|
||||
called. For example, if you call ``model.save()`` or ``model.delete()``, the
|
||||
change will be committed immediately.
|
||||
Django's default behavior is to run in autocommit mode. Each query is
|
||||
immediately committed to the database. :ref:`See below for details
|
||||
<autocommit-details>`.
|
||||
|
||||
This is much like the auto-commit setting for most databases. As soon as you
|
||||
perform an action that needs to write to the database, Django produces the
|
||||
``INSERT``/``UPDATE``/``DELETE`` statements and then does the ``COMMIT``.
|
||||
There's no implicit ``ROLLBACK``.
|
||||
Django uses transactions or savepoints automatically to guarantee the
|
||||
integrity of ORM operations that require multiple queries, especially
|
||||
:ref:`delete() <topics-db-queries-delete>` and :ref:`update()
|
||||
<topics-db-queries-update>` queries.
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
Previous version of Django featured :ref:`a more complicated default
|
||||
behavior <transactions-upgrading-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 request and response phases via Django's ``TransactionMiddleware``.
|
||||
A common way to handle transactions on the web is to wrap each request in a
|
||||
transaction. Set :setting:`ATOMIC_REQUESTS <DATABASE-ATOMIC_REQUESTS>` to
|
||||
``True`` in the configuration of each database for which you want to enable
|
||||
this behavior.
|
||||
|
||||
It works like this: When a request starts, Django starts a transaction. If the
|
||||
response is produced without problems, Django commits any pending transactions.
|
||||
If the view function produces an exception, Django rolls back any pending
|
||||
transactions.
|
||||
It works like this. When a request starts, Django starts a transaction. If the
|
||||
response is produced without problems, Django commits the transaction. If the
|
||||
view function produces an exception, Django rolls back the transaction.
|
||||
Middleware always runs outside of this transaction.
|
||||
|
||||
To activate this feature, just add the ``TransactionMiddleware`` middleware to
|
||||
your :setting:`MIDDLEWARE_CLASSES` setting::
|
||||
You may perfom partial commits and rollbacks in your view code, typically with
|
||||
the :func:`atomic` context manager. However, at the end of the view, either
|
||||
all the changes will be committed, or none of them.
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.middleware.cache.UpdateCacheMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.transaction.TransactionMiddleware',
|
||||
'django.middleware.cache.FetchFromCacheMiddleware',
|
||||
)
|
||||
To disable this behavior for a specific view, you must set the
|
||||
``transactions_per_request`` attribute of the view function itself to
|
||||
``False``, like this::
|
||||
|
||||
The order is quite important. The transaction middleware applies not only to
|
||||
view functions, but also for all middleware modules that come after it. So if
|
||||
you use the session middleware after the transaction middleware, session
|
||||
creation will be part of the transaction.
|
||||
def my_view(request):
|
||||
do_stuff()
|
||||
my_view.transactions_per_request = False
|
||||
|
||||
The various cache middlewares are an exception:
|
||||
``CacheMiddleware``, :class:`~django.middleware.cache.UpdateCacheMiddleware`,
|
||||
and :class:`~django.middleware.cache.FetchFromCacheMiddleware` are never
|
||||
affected. Even when using database caching, Django's cache backend uses its own
|
||||
database cursor (which is mapped to its own database connection internally).
|
||||
.. warning::
|
||||
|
||||
.. note::
|
||||
While the simplicity of this transaction model is appealing, it also makes it
|
||||
inefficient when traffic increases. Opening a transaction for every view has
|
||||
some overhead. The impact on performance depends on the query patterns of your
|
||||
application and on how well your database handles locking.
|
||||
|
||||
The ``TransactionMiddleware`` only affects the database aliased
|
||||
as "default" within your :setting:`DATABASES` setting. If you are using
|
||||
multiple databases and want transaction control over databases other than
|
||||
"default", you will need to write your own transaction middleware.
|
||||
.. admonition:: Per-request transactions and streaming responses
|
||||
|
||||
.. _transaction-management-functions:
|
||||
When a view returns a :class:`~django.http.StreamingHttpResponse`, reading
|
||||
the contents of the response will often execute code to generate the
|
||||
content. Since the view has already returned, such code runs outside of
|
||||
the transaction.
|
||||
|
||||
Controlling transaction management in views
|
||||
===========================================
|
||||
Generally speaking, it isn't advisable to write to the database while
|
||||
generating a streaming response, since there's no sensible way to handle
|
||||
errors after starting to send the response.
|
||||
|
||||
For most people, implicit request-based transactions work wonderfully. However,
|
||||
if you need more fine-grained control over how transactions are managed, you can
|
||||
use a set of functions in ``django.db.transaction`` to control transactions on a
|
||||
per-function or per-code-block basis.
|
||||
In practice, this feature simply wraps every view function in the :func:`atomic`
|
||||
decorator described below.
|
||||
|
||||
These functions, described in detail below, can be used in two different ways:
|
||||
Note that only the execution of your view in enclosed in the transactions.
|
||||
Middleware run outside of the transaction, and so does the rendering of
|
||||
template responses.
|
||||
|
||||
* As a decorator_ on a particular function. For example::
|
||||
.. versionchanged:: 1.6
|
||||
Django used to provide this feature via ``TransactionMiddleware``, which is
|
||||
now deprecated.
|
||||
|
||||
from django.db import transaction
|
||||
Controlling transactions explicitly
|
||||
-----------------------------------
|
||||
|
||||
@transaction.commit_on_success
|
||||
def viewfunc(request):
|
||||
# ...
|
||||
# this code executes inside a transaction
|
||||
# ...
|
||||
.. versionadded:: 1.6
|
||||
|
||||
* As a `context manager`_ around a particular block of code::
|
||||
Django provides a single API to control database transactions.
|
||||
|
||||
from django.db import transaction
|
||||
.. function:: atomic(using=None, savepoint=True)
|
||||
|
||||
def viewfunc(request):
|
||||
# ...
|
||||
# this code executes using default transaction management
|
||||
# ...
|
||||
This function creates an atomic block for writes to the database.
|
||||
(Atomicity is the defining property of database transactions.)
|
||||
|
||||
with transaction.commit_on_success():
|
||||
# ...
|
||||
# this code executes inside a transaction
|
||||
# ...
|
||||
When the block completes successfully, the changes are committed to the
|
||||
database. When it raises an exception, the changes are rolled back.
|
||||
|
||||
Both techniques work with all supported version of Python.
|
||||
``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.
|
||||
|
||||
.. _decorator: http://docs.python.org/glossary.html#term-decorator
|
||||
.. _context manager: http://docs.python.org/glossary.html#term-context-manager
|
||||
``atomic`` takes a ``using`` argument which should be the name of a
|
||||
database. If this argument isn't provided, Django uses the ``"default"``
|
||||
database.
|
||||
|
||||
For maximum compatibility, all of the examples below show transactions using the
|
||||
decorator syntax, but all of the follow functions may be used as context
|
||||
managers, too.
|
||||
|
||||
.. note::
|
||||
|
||||
Although the examples below use view functions as examples, these
|
||||
decorators and context managers can be used anywhere in your code
|
||||
that you need to deal with transactions.
|
||||
|
||||
.. _topics-db-transactions-autocommit:
|
||||
|
||||
.. function:: autocommit
|
||||
|
||||
Use the ``autocommit`` decorator to switch a view function to Django's
|
||||
default commit behavior, regardless of the global transaction setting.
|
||||
|
||||
Example::
|
||||
``atomic`` is usable both as a `decorator`_::
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
@transaction.autocommit
|
||||
@transaction.atomic
|
||||
def viewfunc(request):
|
||||
....
|
||||
# This code executes inside a transaction.
|
||||
do_stuff()
|
||||
|
||||
@transaction.autocommit(using="my_other_database")
|
||||
def viewfunc2(request):
|
||||
....
|
||||
|
||||
Within ``viewfunc()``, transactions will be committed as soon as you call
|
||||
``model.save()``, ``model.delete()``, or any other function that writes to
|
||||
the database. ``viewfunc2()`` will have this same behavior, but for the
|
||||
``"my_other_database"`` connection.
|
||||
|
||||
.. function:: commit_on_success
|
||||
|
||||
Use the ``commit_on_success`` decorator to use a single transaction for all
|
||||
the work done in a function::
|
||||
and as a `context manager`_::
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
@transaction.commit_on_success
|
||||
def viewfunc(request):
|
||||
....
|
||||
# This code executes in autocommit mode (Django's default).
|
||||
do_stuff()
|
||||
|
||||
@transaction.commit_on_success(using="my_other_database")
|
||||
def viewfunc2(request):
|
||||
....
|
||||
with transaction.atomic():
|
||||
# This code executes inside a transaction.
|
||||
do_more_stuff()
|
||||
|
||||
If the function returns successfully, then Django will commit all work done
|
||||
within the function at that point. If the function raises an exception,
|
||||
though, Django will roll back the transaction.
|
||||
.. _decorator: http://docs.python.org/glossary.html#term-decorator
|
||||
.. _context manager: http://docs.python.org/glossary.html#term-context-manager
|
||||
|
||||
.. function:: commit_manually
|
||||
Wrapping ``atomic`` in a try/except block allows for natural handling of
|
||||
integrity errors::
|
||||
|
||||
Use the ``commit_manually`` decorator if you need full control over
|
||||
transactions. It tells Django you'll be managing the transaction on your
|
||||
own.
|
||||
from django.db import IntegrityError, transaction
|
||||
|
||||
Whether you are writing or simply reading from the database, you must
|
||||
``commit()`` or ``rollback()`` explicitly or Django will raise a
|
||||
:exc:`TransactionManagementError` exception. This is required when reading
|
||||
from the database because ``SELECT`` statements may call functions which
|
||||
modify tables, and thus it is impossible to know if any data has been
|
||||
modified.
|
||||
|
||||
Manual transaction management looks like this::
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
@transaction.commit_manually
|
||||
@transaction.atomic
|
||||
def viewfunc(request):
|
||||
...
|
||||
# You can commit/rollback however and whenever you want
|
||||
transaction.commit()
|
||||
...
|
||||
do_stuff()
|
||||
|
||||
# But you've got to remember to do it yourself!
|
||||
try:
|
||||
...
|
||||
except:
|
||||
transaction.rollback()
|
||||
else:
|
||||
transaction.commit()
|
||||
with transaction.atomic():
|
||||
do_stuff_that_could_fail()
|
||||
except IntegrityError:
|
||||
handle_exception()
|
||||
|
||||
@transaction.commit_manually(using="my_other_database")
|
||||
def viewfunc2(request):
|
||||
....
|
||||
do_more_stuff()
|
||||
|
||||
.. _topics-db-transactions-requirements:
|
||||
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.
|
||||
|
||||
Requirements for transaction handling
|
||||
=====================================
|
||||
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.
|
||||
|
||||
Django requires that every transaction that is opened is closed before
|
||||
the completion of a request. If you are using :func:`autocommit` (the
|
||||
default commit mode) or :func:`commit_on_success`, this will be done
|
||||
for you automatically (with the exception of :ref:`executing custom SQL
|
||||
<executing-custom-sql>`). However, if you are manually managing
|
||||
transactions (using the :func:`commit_manually` decorator), you must
|
||||
ensure that the transaction is either committed or rolled back before
|
||||
a request is completed.
|
||||
``atomic`` can only be used in autocommit mode. It will raise an exception
|
||||
if autocommit is turned off.
|
||||
|
||||
This applies to all database operations, not just write operations. Even
|
||||
if your transaction only reads from the database, the transaction must
|
||||
be committed or rolled back before you complete a request.
|
||||
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.
|
||||
|
||||
You can disable the creation of savepoints for inner blocks by setting the
|
||||
``savepoint`` argument to ``False``. If an exception occurs, Django will
|
||||
perform the rollback when exiting the first parent block with a savepoint
|
||||
if there is one, and the outermost block otherwise. Atomicity is still
|
||||
guaranteed by the outer transaction. This option should only be used if
|
||||
the overhead of savepoints is noticeable. It has the drawback of breaking
|
||||
the error handling described above.
|
||||
|
||||
.. admonition:: Performance considerations
|
||||
|
||||
Open transactions have a performance cost for your database server. To
|
||||
minimize this overhead, keep your transactions as short as possible. This
|
||||
is especially important of you're using :func:`atomic` in long-running
|
||||
processes, outside of Django's request / response cycle.
|
||||
|
||||
Autocommit
|
||||
==========
|
||||
|
||||
.. _autocommit-details:
|
||||
|
||||
Why Django uses autocommit
|
||||
--------------------------
|
||||
|
||||
In the SQL standards, each SQL query starts a transaction, unless one is
|
||||
already in progress. Such transactions must then be committed or rolled back.
|
||||
|
||||
This isn't always convenient for application developers. To alleviate this
|
||||
problem, most databases provide an autocommit mode. When autocommit is turned
|
||||
on, each SQL query is wrapped in its own transaction. In other words, the
|
||||
transaction is not only automatically started, but also automatically
|
||||
committed.
|
||||
|
||||
:pep:`249`, the Python Database API Specification v2.0, requires autocommit to
|
||||
be initially turned off. Django overrides this default and turns autocommit
|
||||
on.
|
||||
|
||||
To avoid this, you can :ref:`deactivate the transaction management
|
||||
<deactivate-transaction-management>`, but it isn't recommended.
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
Before Django 1.6, autocommit was turned off, and it was emulated by
|
||||
forcing a commit after write operations in the ORM.
|
||||
|
||||
.. warning::
|
||||
|
||||
If you're using the database API directly — for instance, you're running
|
||||
SQL queries with ``cursor.execute()`` — be aware that autocommit is on,
|
||||
and consider wrapping your operations in a transaction, with
|
||||
:func:`atomic`, to ensure consistency.
|
||||
|
||||
.. _deactivate-transaction-management:
|
||||
|
||||
How to globally deactivate transaction management
|
||||
=================================================
|
||||
Deactivating transaction management
|
||||
-----------------------------------
|
||||
|
||||
Control freaks can totally disable all transaction management by setting
|
||||
:setting:`TRANSACTIONS_MANAGED` to ``True`` in the Django settings file.
|
||||
You can totally disable Django's transaction management for a given database
|
||||
by setting :setting:`AUTOCOMMIT <DATABASE-AUTOCOMMIT>` to ``False`` in its
|
||||
configuration. If you do this, Django won't enable autocommit, and won't
|
||||
perform any commits. You'll get the regular behavior of the underlying
|
||||
database library.
|
||||
|
||||
If you do this, Django won't provide any automatic transaction management
|
||||
whatsoever. Middleware will no longer implicitly commit transactions, and
|
||||
you'll need to roll management yourself. This even requires you to commit
|
||||
changes done by middleware somewhere else.
|
||||
This requires you to commit explicitly every transaction, even those started
|
||||
by Django or by third-party libraries. Thus, this is best used in situations
|
||||
where you want to run your own transaction-controlling middleware or do
|
||||
something really strange.
|
||||
|
||||
Thus, this is best used in situations where you want to run your own
|
||||
transaction-controlling middleware or do something really strange. In almost
|
||||
all situations, you'll be better off using the default behavior, or the
|
||||
transaction middleware, and only modify selected functions as needed.
|
||||
.. versionchanged:: 1.6
|
||||
This used to be controlled by the ``TRANSACTIONS_MANAGED`` setting.
|
||||
|
||||
Low-level APIs
|
||||
==============
|
||||
|
||||
.. warning::
|
||||
|
||||
Always prefer :func:`atomic` if possible at all. It accounts for the
|
||||
idiosyncrasies of each database and prevents invalid operations.
|
||||
|
||||
The low level APIs are only useful if you're implementing your own
|
||||
transaction management.
|
||||
|
||||
.. _managing-autocommit:
|
||||
|
||||
Autocommit
|
||||
----------
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
Django provides a straightforward API to manage the autocommit state of each
|
||||
database connection, if you need to.
|
||||
|
||||
.. function:: get_autocommit(using=None)
|
||||
|
||||
.. function:: set_autocommit(autocommit, using=None)
|
||||
|
||||
These functions take a ``using`` argument which should be the name of a
|
||||
database. If it isn't provided, Django uses the ``"default"`` database.
|
||||
|
||||
Autocommit is initially turned on. If you turn it off, it's your
|
||||
responsibility to restore it.
|
||||
|
||||
Once you turn autocommit off, you get the default behavior of your database
|
||||
adapter, and Django won't help you. Although that behavior is specified in
|
||||
:pep:`249`, implementations of adapters aren't always consistent with one
|
||||
another. Review the documentation of the adapter you're using carefully.
|
||||
|
||||
You must ensure that no transaction is active, usually by issuing a
|
||||
:func:`commit` or a :func:`rollback`, before turning autocommit back on.
|
||||
|
||||
:func:`atomic` requires autocommit to be turned on; it will raise an exception
|
||||
if autocommit is off. Django will also refuse to turn autocommit off when an
|
||||
:func:`atomic` block is active, because that would break atomicity.
|
||||
|
||||
Transactions
|
||||
------------
|
||||
|
||||
A transaction is an atomic set of database queries. Even if your program
|
||||
crashes, the database guarantees that either all the changes will be applied,
|
||||
or none of them.
|
||||
|
||||
Django doesn't provide an API to start a transaction. The expected way to
|
||||
start a transaction is to disable autocommit with :func:`set_autocommit`.
|
||||
|
||||
Once you're in a transaction, you can choose either to apply the changes
|
||||
you've performed until this point with :func:`commit`, or to cancel them with
|
||||
:func:`rollback`.
|
||||
|
||||
.. function:: commit(using=None)
|
||||
|
||||
.. function:: rollback(using=None)
|
||||
|
||||
These functions take a ``using`` argument which should be the name of a
|
||||
database. If it isn't provided, Django uses the ``"default"`` database.
|
||||
|
||||
Django will refuse to commit or to rollback when an :func:`atomic` block is
|
||||
active, because that would break atomicity.
|
||||
|
||||
.. _topics-db-transactions-savepoints:
|
||||
|
||||
Savepoints
|
||||
==========
|
||||
----------
|
||||
|
||||
A savepoint is a marker within a transaction that enables you to roll back part
|
||||
of a transaction, rather than the full transaction. Savepoints are available
|
||||
with the PostgreSQL 8, Oracle and MySQL (when using the InnoDB storage engine)
|
||||
backends. Other backends provide the savepoint functions, but they're empty
|
||||
operations -- they don't actually do anything.
|
||||
A savepoint is a marker within a transaction that enables you to roll back
|
||||
part of a transaction, rather than the full transaction. Savepoints are
|
||||
available with the SQLite (≥ 3.6.8), PostgreSQL, Oracle and MySQL (when using
|
||||
the InnoDB storage engine) backends. Other backends provide the savepoint
|
||||
functions, but they're empty operations -- they don't actually do anything.
|
||||
|
||||
Savepoints aren't especially useful if you are using the default
|
||||
``autocommit`` behavior of Django. However, if you are using
|
||||
``commit_on_success`` or ``commit_manually``, each open transaction will build
|
||||
up a series of database operations, awaiting a commit or rollback. If you
|
||||
issue a rollback, the entire transaction is rolled back. Savepoints provide
|
||||
the ability to perform a fine-grained rollback, rather than the full rollback
|
||||
that would be performed by ``transaction.rollback()``.
|
||||
Savepoints aren't especially useful if you are using autocommit, the default
|
||||
behavior of Django. However, once you open a transaction with :func:`atomic`,
|
||||
you build up a series of database operations awaiting a commit or rollback. If
|
||||
you issue a rollback, the entire transaction is rolled back. Savepoints
|
||||
provide the ability to perform a fine-grained rollback, rather than the full
|
||||
rollback that would be performed by ``transaction.rollback()``.
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
|
||||
When the :func:`atomic` decorator is nested, it creates a savepoint to allow
|
||||
partial commit or rollback. You're strongly encouraged to use :func:`atomic`
|
||||
rather than the functions described below, but they're still part of the
|
||||
public API, and there's no plan to deprecate them.
|
||||
|
||||
Each of these functions takes a ``using`` argument which should be the name of
|
||||
a database for which the behavior applies. If no ``using`` argument is
|
||||
provided then the ``"default"`` database is used.
|
||||
|
||||
Savepoints are controlled by three methods on the transaction object:
|
||||
Savepoints are controlled by three functions in :mod:`django.db.transaction`:
|
||||
|
||||
.. method:: transaction.savepoint(using=None)
|
||||
.. function:: savepoint(using=None)
|
||||
|
||||
Creates a new savepoint. This marks a point in the transaction that
|
||||
is known to be in a "good" state.
|
||||
|
||||
Returns the savepoint ID (sid).
|
||||
Returns the savepoint ID (``sid``).
|
||||
|
||||
.. method:: transaction.savepoint_commit(sid, using=None)
|
||||
.. function:: savepoint_commit(sid, using=None)
|
||||
|
||||
Updates the savepoint to include any operations that have been performed
|
||||
since the savepoint was created, or since the last commit.
|
||||
Releases savepoint ``sid``. The changes performed since the savepoint was
|
||||
created become part of the transaction.
|
||||
|
||||
.. method:: transaction.savepoint_rollback(sid, using=None)
|
||||
.. function:: savepoint_rollback(sid, using=None)
|
||||
|
||||
Rolls the transaction back to the last point at which the savepoint was
|
||||
committed.
|
||||
Rolls back the transaction to savepoint ``sid``.
|
||||
|
||||
These functions do nothing if savepoints aren't supported or if the database
|
||||
is in autocommit mode.
|
||||
|
||||
In addition, there's a utility function:
|
||||
|
||||
.. function:: clean_savepoints(using=None)
|
||||
|
||||
Resets the counter used to generate unique savepoint IDs.
|
||||
|
||||
The following example demonstrates the use of savepoints::
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
@transaction.commit_manually
|
||||
# open a transaction
|
||||
@transaction.atomic
|
||||
def viewfunc(request):
|
||||
|
||||
a.save()
|
||||
# open transaction now contains a.save()
|
||||
# transaction now contains a.save()
|
||||
|
||||
sid = transaction.savepoint()
|
||||
|
||||
b.save()
|
||||
# open transaction now contains a.save() and b.save()
|
||||
# transaction now contains a.save() and b.save()
|
||||
|
||||
if want_to_keep_b:
|
||||
transaction.savepoint_commit(sid)
|
||||
|
@ -289,10 +378,25 @@ The following example demonstrates the use of savepoints::
|
|||
transaction.savepoint_rollback(sid)
|
||||
# open transaction now contains only a.save()
|
||||
|
||||
transaction.commit()
|
||||
Database-specific notes
|
||||
=======================
|
||||
|
||||
Savepoints in SQLite
|
||||
--------------------
|
||||
|
||||
While SQLite ≥ 3.6.8 supports savepoints, a flaw in the design of the
|
||||
:mod:`sqlite3` makes them hardly usable.
|
||||
|
||||
When autocommit is enabled, savepoints don't make sense. When it's disabled,
|
||||
:mod:`sqlite3` commits implicitly before savepoint-related statement. (It
|
||||
commits before any statement other than ``SELECT``, ``INSERT``, ``UPDATE``,
|
||||
``DELETE`` and ``REPLACE``.)
|
||||
|
||||
As a consequence, savepoints are only usable inside a transaction ie. inside
|
||||
an :func:`atomic` block.
|
||||
|
||||
Transactions in MySQL
|
||||
=====================
|
||||
---------------------
|
||||
|
||||
If you're using MySQL, your tables may or may not support transactions; it
|
||||
depends on your MySQL version and the table types you're using. (By
|
||||
|
@ -301,14 +405,14 @@ peculiarities are outside the scope of this article, but the MySQL site has
|
|||
`information on MySQL transactions`_.
|
||||
|
||||
If your MySQL setup does *not* support transactions, then Django will function
|
||||
in auto-commit mode: Statements will be executed and committed as soon as
|
||||
in autocommit mode: Statements will be executed and committed as soon as
|
||||
they're called. If your MySQL setup *does* support transactions, Django will
|
||||
handle transactions as explained in this document.
|
||||
|
||||
.. _information on MySQL transactions: http://dev.mysql.com/doc/refman/5.0/en/sql-syntax-transactions.html
|
||||
|
||||
Handling exceptions within PostgreSQL transactions
|
||||
==================================================
|
||||
--------------------------------------------------
|
||||
|
||||
When a call to a PostgreSQL cursor raises an exception (typically
|
||||
``IntegrityError``), all subsequent SQL in the same transaction will fail with
|
||||
|
@ -321,7 +425,7 @@ force_insert/force_update flag, or invoking custom SQL.
|
|||
There are several ways to recover from this sort of error.
|
||||
|
||||
Transaction rollback
|
||||
--------------------
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The first option is to roll back the entire transaction. For example::
|
||||
|
||||
|
@ -338,13 +442,13 @@ made by ``a.save()`` would be lost, even though that operation raised no error
|
|||
itself.
|
||||
|
||||
Savepoint rollback
|
||||
------------------
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you are using PostgreSQL 8 or later, you can use :ref:`savepoints
|
||||
<topics-db-transactions-savepoints>` to control the extent of a rollback.
|
||||
Before performing a database operation that could fail, you can set or update
|
||||
the savepoint; that way, if the operation fails, you can roll back the single
|
||||
offending operation, rather than the entire transaction. For example::
|
||||
You can use :ref:`savepoints <topics-db-transactions-savepoints>` to control
|
||||
the extent of a rollback. Before performing a database operation that could
|
||||
fail, you can set or update the savepoint; that way, if the operation fails,
|
||||
you can roll back the single offending operation, rather than the entire
|
||||
transaction. For example::
|
||||
|
||||
a.save() # Succeeds, and never undone by savepoint rollback
|
||||
try:
|
||||
|
@ -358,25 +462,227 @@ offending operation, rather than the entire transaction. For example::
|
|||
In this example, ``a.save()`` will not be undone in the case where
|
||||
``b.save()`` raises an exception.
|
||||
|
||||
Database-level autocommit
|
||||
-------------------------
|
||||
.. _transactions-upgrading-from-1.5:
|
||||
|
||||
With PostgreSQL 8.2 or later, there is an advanced option to run PostgreSQL
|
||||
with :doc:`database-level autocommit </ref/databases>`. If you use this option,
|
||||
there is no constantly open transaction, so it is always possible to continue
|
||||
after catching an exception. For example::
|
||||
Changes from Django 1.5 and earlier
|
||||
===================================
|
||||
|
||||
a.save() # succeeds
|
||||
The features described below were deprecated in Django 1.6 and will be removed
|
||||
in Django 1.8. They're documented in order to ease the migration to the new
|
||||
transaction management APIs.
|
||||
|
||||
Legacy APIs
|
||||
-----------
|
||||
|
||||
The following functions, defined in ``django.db.transaction``, provided a way
|
||||
to control transactions on a per-function or per-code-block basis. They could
|
||||
be used as decorators or as context managers, and they accepted a ``using``
|
||||
argument, exactly like :func:`atomic`.
|
||||
|
||||
.. function:: autocommit
|
||||
|
||||
Enable Django's default autocommit behavior.
|
||||
|
||||
Transactions will be committed as soon as you call ``model.save()``,
|
||||
``model.delete()``, or any other function that writes to the database.
|
||||
|
||||
.. function:: commit_on_success
|
||||
|
||||
Use a single transaction for all the work done in a function.
|
||||
|
||||
If the function returns successfully, then Django will commit all work done
|
||||
within the function at that point. If the function raises an exception,
|
||||
though, Django will roll back the transaction.
|
||||
|
||||
.. function:: commit_manually
|
||||
|
||||
Tells Django you'll be managing the transaction on your own.
|
||||
|
||||
Whether you are writing or simply reading from the database, you must
|
||||
``commit()`` or ``rollback()`` explicitly or Django will raise a
|
||||
:exc:`TransactionManagementError` exception. This is required when reading
|
||||
from the database because ``SELECT`` statements may call functions which
|
||||
modify tables, and thus it is impossible to know if any data has been
|
||||
modified.
|
||||
|
||||
.. _transaction-states:
|
||||
|
||||
Transaction states
|
||||
------------------
|
||||
|
||||
The three functions described above relied on a concept called "transaction
|
||||
states". This mechanisme was deprecated in Django 1.6, but it's still
|
||||
available until Django 1.8..
|
||||
|
||||
At any time, each database connection is in one of these two states:
|
||||
|
||||
- **auto mode**: autocommit is enabled;
|
||||
- **managed mode**: autocommit is disabled.
|
||||
|
||||
Django starts in auto mode. ``TransactionMiddleware``,
|
||||
:func:`commit_on_success` and :func:`commit_manually` activate managed mode;
|
||||
:func:`autocommit` activates auto mode.
|
||||
|
||||
Internally, Django keeps a stack of states. Activations and deactivations must
|
||||
be balanced.
|
||||
|
||||
For example, ``commit_on_success`` switches to managed mode when entering the
|
||||
block of code it controls; when exiting the block, it commits or rollbacks,
|
||||
and switches back to auto mode.
|
||||
|
||||
So :func:`commit_on_success` really has two effects: it changes the
|
||||
transaction state and it defines an transaction block. Nesting will give the
|
||||
expected results in terms of transaction state, but not in terms of
|
||||
transaction semantics. Most often, the inner block will commit, breaking the
|
||||
atomicity of the outer block.
|
||||
|
||||
:func:`autocommit` and :func:`commit_manually` have similar limitations.
|
||||
|
||||
API changes
|
||||
-----------
|
||||
|
||||
Transaction middleware
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In Django 1.6, ``TransactionMiddleware`` is deprecated and replaced
|
||||
:setting:`ATOMIC_REQUESTS <DATABASE-ATOMIC_REQUESTS>`. While the general
|
||||
behavior is the same, there are a few differences.
|
||||
|
||||
With the transaction middleware, it was still possible to switch to autocommit
|
||||
or to commit explicitly in a view. Since :func:`atomic` guarantees atomicity,
|
||||
this isn't allowed any longer.
|
||||
|
||||
To avoid wrapping a particular view in a transaction, instead of::
|
||||
|
||||
@transaction.autocommit
|
||||
def my_view(request):
|
||||
do_stuff()
|
||||
|
||||
you must now use this pattern::
|
||||
|
||||
def my_view(request):
|
||||
do_stuff()
|
||||
my_view.transactions_per_request = False
|
||||
|
||||
The transaction middleware applied not only to view functions, but also to
|
||||
middleware modules that come after it. For instance, if you used the session
|
||||
middleware after the transaction middleware, session creation was part of the
|
||||
transaction. :setting:`ATOMIC_REQUESTS <DATABASE-ATOMIC_REQUESTS>` only
|
||||
applies to the view itself.
|
||||
|
||||
Managing transactions
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Starting with Django 1.6, :func:`atomic` is the only supported API for
|
||||
defining a transaction. Unlike the deprecated APIs, it's nestable and always
|
||||
guarantees atomicity.
|
||||
|
||||
In most cases, it will be a drop-in replacement for :func:`commit_on_success`.
|
||||
|
||||
During the deprecation period, it's possible to use :func:`atomic` within
|
||||
:func:`autocommit`, :func:`commit_on_success` or :func:`commit_manually`.
|
||||
However, the reverse is forbidden, because nesting the old decorators /
|
||||
context managers breaks atomicity.
|
||||
|
||||
If you enter :func:`atomic` while you're in managed mode, it will trigger a
|
||||
commit to start from a clean slate.
|
||||
|
||||
Managing autocommit
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Django 1.6 introduces an explicit :ref:`API for mananging autocommit
|
||||
<managing-autocommit>`.
|
||||
|
||||
To disable autocommit temporarily, instead of::
|
||||
|
||||
with transaction.commit_manually():
|
||||
# do stuff
|
||||
|
||||
you should now use::
|
||||
|
||||
transaction.set_autocommit(False)
|
||||
try:
|
||||
b.save() # Could throw exception
|
||||
except IntegrityError:
|
||||
pass
|
||||
c.save() # succeeds
|
||||
# do stuff
|
||||
finally:
|
||||
transaction.set_autocommit(True)
|
||||
|
||||
.. note::
|
||||
To enable autocommit temporarily, instead of::
|
||||
|
||||
This is not the same as the :ref:`autocommit decorator
|
||||
<topics-db-transactions-autocommit>`. When using database level autocommit
|
||||
there is no database transaction at all. The ``autocommit`` decorator
|
||||
still uses transactions, automatically committing each transaction when
|
||||
a database modifying operation occurs.
|
||||
with transaction.autocommit():
|
||||
# do stuff
|
||||
|
||||
you should now use::
|
||||
|
||||
transaction.set_autocommit(True)
|
||||
try:
|
||||
# do stuff
|
||||
finally:
|
||||
transaction.set_autocommit(False)
|
||||
|
||||
Disabling transaction management
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Instead of setting ``TRANSACTIONS_MANAGED = True``, set the ``AUTOCOMMIT`` key
|
||||
to ``False`` in the configuration of each database, as explained in :ref
|
||||
:`deactivate-transaction-management`.
|
||||
|
||||
Backwards incompatibilities
|
||||
---------------------------
|
||||
|
||||
Since version 1.6, Django uses database-level autocommit in auto mode.
|
||||
Previously, it implemented application-level autocommit by triggering a commit
|
||||
after each ORM write.
|
||||
|
||||
As a consequence, each database query (for instance, an ORM read) started a
|
||||
transaction that lasted until the next ORM write. Such "automatic
|
||||
transactions" no longer exist in Django 1.6.
|
||||
|
||||
There are four known scenarios where this is backwards-incompatible.
|
||||
|
||||
Note that managed mode isn't affected at all. This section assumes auto mode.
|
||||
See the :ref:`description of modes <transaction-states>` above.
|
||||
|
||||
Sequences of custom SQL queries
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you're executing several :ref:`custom SQL queries <executing-custom-sql>`
|
||||
in a row, each one now runs in its own transaction, instead of sharing the
|
||||
same "automatic transaction". If you need to enforce atomicity, you must wrap
|
||||
the sequence of queries in :func:`commit_on_success`.
|
||||
|
||||
To check for this problem, look for calls to ``cursor.execute()``. They're
|
||||
usually followed by a call to ``transaction.commit_unless_managed``, which
|
||||
isn't necessary any more and should be removed.
|
||||
|
||||
Select for update
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you were relying on "automatic transactions" to provide locking between
|
||||
:meth:`~django.db.models.query.QuerySet.select_for_update` and a subsequent
|
||||
write operation — an extremely fragile design, but nonetheless possible — you
|
||||
must wrap the relevant code in :func:`atomic`.
|
||||
|
||||
Using a high isolation level
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you were using the "repeatable read" isolation level or higher, and if you
|
||||
relied on "automatic transactions" to guarantee consistency between successive
|
||||
reads, the new behavior might be backwards-incompatible. To enforce
|
||||
consistency, you must wrap such sequences in :func:`atomic`.
|
||||
|
||||
MySQL defaults to "repeatable read" and SQLite to "serializable"; they may be
|
||||
affected by this problem.
|
||||
|
||||
At the "read committed" isolation level or lower, "automatic transactions"
|
||||
have no effect on the semantics of any sequence of ORM operations.
|
||||
|
||||
PostgreSQL and Oracle default to "read committed" and aren't affected, unless
|
||||
you changed the isolation level.
|
||||
|
||||
Using unsupported database features
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
With triggers, views, or functions, it's possible to make ORM reads result in
|
||||
database modifications. Django 1.5 and earlier doesn't deal with this case and
|
||||
it's theoretically possible to observe a different behavior after upgrading to
|
||||
Django 1.6 or later. In doubt, use :func:`atomic` to enforce integrity.
|
||||
|
|
|
@ -302,8 +302,8 @@ class PostgresNewConnectionTest(TestCase):
|
|||
transaction is rolled back.
|
||||
"""
|
||||
@unittest.skipUnless(
|
||||
connection.vendor == 'postgresql' and connection.isolation_level > 0,
|
||||
"This test applies only to PostgreSQL without autocommit")
|
||||
connection.vendor == 'postgresql',
|
||||
"This test applies only to PostgreSQL")
|
||||
def test_connect_and_rollback(self):
|
||||
new_connections = ConnectionHandler(settings.DATABASES)
|
||||
new_connection = new_connections[DEFAULT_DB_ALIAS]
|
||||
|
@ -522,7 +522,8 @@ class FkConstraintsTests(TransactionTestCase):
|
|||
"""
|
||||
When constraint checks are disabled, should be able to write bad data without IntegrityErrors.
|
||||
"""
|
||||
with transaction.commit_manually():
|
||||
transaction.set_autocommit(False)
|
||||
try:
|
||||
# Create an Article.
|
||||
models.Article.objects.create(headline="Test article", pub_date=datetime.datetime(2010, 9, 4), reporter=self.r)
|
||||
# Retrive it from the DB
|
||||
|
@ -536,12 +537,15 @@ class FkConstraintsTests(TransactionTestCase):
|
|||
self.fail("IntegrityError should not have occurred.")
|
||||
finally:
|
||||
transaction.rollback()
|
||||
finally:
|
||||
transaction.set_autocommit(True)
|
||||
|
||||
def test_disable_constraint_checks_context_manager(self):
|
||||
"""
|
||||
When constraint checks are disabled (using context manager), should be able to write bad data without IntegrityErrors.
|
||||
"""
|
||||
with transaction.commit_manually():
|
||||
transaction.set_autocommit(False)
|
||||
try:
|
||||
# Create an Article.
|
||||
models.Article.objects.create(headline="Test article", pub_date=datetime.datetime(2010, 9, 4), reporter=self.r)
|
||||
# Retrive it from the DB
|
||||
|
@ -554,12 +558,15 @@ class FkConstraintsTests(TransactionTestCase):
|
|||
self.fail("IntegrityError should not have occurred.")
|
||||
finally:
|
||||
transaction.rollback()
|
||||
finally:
|
||||
transaction.set_autocommit(True)
|
||||
|
||||
def test_check_constraints(self):
|
||||
"""
|
||||
Constraint checks should raise an IntegrityError when bad data is in the DB.
|
||||
"""
|
||||
with transaction.commit_manually():
|
||||
try:
|
||||
transaction.set_autocommit(False)
|
||||
# Create an Article.
|
||||
models.Article.objects.create(headline="Test article", pub_date=datetime.datetime(2010, 9, 4), reporter=self.r)
|
||||
# Retrive it from the DB
|
||||
|
@ -572,6 +579,8 @@ class FkConstraintsTests(TransactionTestCase):
|
|||
connection.check_constraints()
|
||||
finally:
|
||||
transaction.rollback()
|
||||
finally:
|
||||
transaction.set_autocommit(True)
|
||||
|
||||
|
||||
class ThreadTests(TestCase):
|
||||
|
|
|
@ -22,9 +22,7 @@ class DeleteLockingTest(TransactionTestCase):
|
|||
self.conn2 = new_connections[DEFAULT_DB_ALIAS]
|
||||
# Put both DB connections into managed transaction mode
|
||||
transaction.enter_transaction_management()
|
||||
transaction.managed(True)
|
||||
self.conn2.enter_transaction_management()
|
||||
self.conn2.managed(True)
|
||||
|
||||
def tearDown(self):
|
||||
# Close down the second connection.
|
||||
|
@ -335,7 +333,7 @@ class Ticket19102Tests(TestCase):
|
|||
).select_related('orgunit').delete()
|
||||
self.assertFalse(Login.objects.filter(pk=self.l1.pk).exists())
|
||||
self.assertTrue(Login.objects.filter(pk=self.l2.pk).exists())
|
||||
|
||||
|
||||
@skipUnlessDBFeature("update_can_self_select")
|
||||
def test_ticket_19102_defer(self):
|
||||
with self.assertNumQueries(1):
|
||||
|
|
|
@ -25,7 +25,8 @@ class SampleTestCase(TestCase):
|
|||
|
||||
class TestNoInitialDataLoading(TransactionTestCase):
|
||||
def test_syncdb(self):
|
||||
with transaction.commit_manually():
|
||||
transaction.set_autocommit(False)
|
||||
try:
|
||||
Book.objects.all().delete()
|
||||
|
||||
management.call_command(
|
||||
|
@ -35,6 +36,9 @@ class TestNoInitialDataLoading(TransactionTestCase):
|
|||
)
|
||||
self.assertQuerysetEqual(Book.objects.all(), [])
|
||||
transaction.rollback()
|
||||
finally:
|
||||
transaction.set_autocommit(True)
|
||||
|
||||
|
||||
def test_flush(self):
|
||||
# Test presence of fixture (flush called by TransactionTestCase)
|
||||
|
@ -45,7 +49,8 @@ class TestNoInitialDataLoading(TransactionTestCase):
|
|||
lambda a: a.name
|
||||
)
|
||||
|
||||
with transaction.commit_manually():
|
||||
transaction.set_autocommit(False)
|
||||
try:
|
||||
management.call_command(
|
||||
'flush',
|
||||
verbosity=0,
|
||||
|
@ -55,6 +60,8 @@ class TestNoInitialDataLoading(TransactionTestCase):
|
|||
)
|
||||
self.assertQuerysetEqual(Book.objects.all(), [])
|
||||
transaction.rollback()
|
||||
finally:
|
||||
transaction.set_autocommit(True)
|
||||
|
||||
|
||||
class FixtureTestCase(TestCase):
|
||||
|
|
|
@ -684,5 +684,8 @@ class TestTicket11101(TransactionTestCase):
|
|||
@skipUnlessDBFeature('supports_transactions')
|
||||
def test_ticket_11101(self):
|
||||
"""Test that fixtures can be rolled back (ticket #11101)."""
|
||||
ticket_11101 = transaction.commit_manually(self.ticket_11101)
|
||||
ticket_11101()
|
||||
transaction.set_autocommit(False)
|
||||
try:
|
||||
self.ticket_11101()
|
||||
finally:
|
||||
transaction.set_autocommit(True)
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
from django.core.handlers.wsgi import WSGIHandler
|
||||
from django.core.signals import request_started, request_finished
|
||||
from django.db import close_old_connections
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.db import close_old_connections, connection
|
||||
from django.test import RequestFactory, TestCase, TransactionTestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.utils import six
|
||||
|
||||
|
||||
class HandlerTests(TestCase):
|
||||
|
@ -37,6 +36,31 @@ class HandlerTests(TestCase):
|
|||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
|
||||
class TransactionsPerRequestTests(TransactionTestCase):
|
||||
urls = 'handlers.urls'
|
||||
|
||||
def test_no_transaction(self):
|
||||
response = self.client.get('/in_transaction/')
|
||||
self.assertContains(response, 'False')
|
||||
|
||||
def test_auto_transaction(self):
|
||||
old_atomic_requests = connection.settings_dict['ATOMIC_REQUESTS']
|
||||
try:
|
||||
connection.settings_dict['ATOMIC_REQUESTS'] = True
|
||||
response = self.client.get('/in_transaction/')
|
||||
finally:
|
||||
connection.settings_dict['ATOMIC_REQUESTS'] = old_atomic_requests
|
||||
self.assertContains(response, 'True')
|
||||
|
||||
def test_no_auto_transaction(self):
|
||||
old_atomic_requests = connection.settings_dict['ATOMIC_REQUESTS']
|
||||
try:
|
||||
connection.settings_dict['ATOMIC_REQUESTS'] = True
|
||||
response = self.client.get('/not_in_transaction/')
|
||||
finally:
|
||||
connection.settings_dict['ATOMIC_REQUESTS'] = old_atomic_requests
|
||||
self.assertContains(response, 'False')
|
||||
|
||||
class SignalsTests(TestCase):
|
||||
urls = 'handlers.urls'
|
||||
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import patterns, url
|
||||
from django.http import HttpResponse, StreamingHttpResponse
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^regular/$', lambda request: HttpResponse(b"regular content")),
|
||||
url(r'^streaming/$', lambda request: StreamingHttpResponse([b"streaming", b" ", b"content"])),
|
||||
url(r'^regular/$', views.regular),
|
||||
url(r'^streaming/$', views.streaming),
|
||||
url(r'^in_transaction/$', views.in_transaction),
|
||||
url(r'^not_in_transaction/$', views.not_in_transaction),
|
||||
)
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import connection
|
||||
from django.http import HttpResponse, StreamingHttpResponse
|
||||
|
||||
def regular(request):
|
||||
return HttpResponse(b"regular content")
|
||||
|
||||
def streaming(request):
|
||||
return StreamingHttpResponse([b"streaming", b" ", b"content"])
|
||||
|
||||
def in_transaction(request):
|
||||
return HttpResponse(str(connection.in_atomic_block))
|
||||
|
||||
def not_in_transaction(request):
|
||||
return HttpResponse(str(connection.in_atomic_block))
|
||||
not_in_transaction.transactions_per_request = False
|
|
@ -22,6 +22,9 @@ from django.test.utils import override_settings
|
|||
from django.utils import six
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.six.moves import xrange
|
||||
from django.utils.unittest import expectedFailure
|
||||
|
||||
from transactions.tests import IgnorePendingDeprecationWarningsMixin
|
||||
|
||||
from .models import Band
|
||||
|
||||
|
@ -669,11 +672,12 @@ class ETagGZipMiddlewareTest(TestCase):
|
|||
|
||||
self.assertNotEqual(gzip_etag, nogzip_etag)
|
||||
|
||||
class TransactionMiddlewareTest(TransactionTestCase):
|
||||
class TransactionMiddlewareTest(IgnorePendingDeprecationWarningsMixin, TransactionTestCase):
|
||||
"""
|
||||
Test the transaction middleware.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TransactionMiddlewareTest, self).setUp()
|
||||
self.request = HttpRequest()
|
||||
self.request.META = {
|
||||
'SERVER_NAME': 'testserver',
|
||||
|
@ -685,33 +689,22 @@ class TransactionMiddlewareTest(TransactionTestCase):
|
|||
|
||||
def tearDown(self):
|
||||
transaction.abort()
|
||||
super(TransactionMiddlewareTest, self).tearDown()
|
||||
|
||||
def test_request(self):
|
||||
TransactionMiddleware().process_request(self.request)
|
||||
self.assertTrue(transaction.is_managed())
|
||||
self.assertFalse(transaction.get_autocommit())
|
||||
|
||||
def test_managed_response(self):
|
||||
transaction.enter_transaction_management()
|
||||
transaction.managed(True)
|
||||
Band.objects.create(name='The Beatles')
|
||||
self.assertTrue(transaction.is_dirty())
|
||||
TransactionMiddleware().process_response(self.request, self.response)
|
||||
self.assertFalse(transaction.is_dirty())
|
||||
self.assertEqual(Band.objects.count(), 1)
|
||||
|
||||
def test_unmanaged_response(self):
|
||||
transaction.enter_transaction_management()
|
||||
transaction.managed(False)
|
||||
self.assertEqual(Band.objects.count(), 0)
|
||||
TransactionMiddleware().process_response(self.request, self.response)
|
||||
self.assertFalse(transaction.is_managed())
|
||||
# The transaction middleware doesn't commit/rollback if management
|
||||
# has been disabled.
|
||||
self.assertTrue(transaction.is_dirty())
|
||||
|
||||
def test_exception(self):
|
||||
transaction.enter_transaction_management()
|
||||
transaction.managed(True)
|
||||
Band.objects.create(name='The Beatles')
|
||||
self.assertTrue(transaction.is_dirty())
|
||||
TransactionMiddleware().process_exception(self.request, None)
|
||||
|
@ -726,7 +719,6 @@ class TransactionMiddlewareTest(TransactionTestCase):
|
|||
raise IntegrityError()
|
||||
connections[DEFAULT_DB_ALIAS].commit = raise_exception
|
||||
transaction.enter_transaction_management()
|
||||
transaction.managed(True)
|
||||
Band.objects.create(name='The Beatles')
|
||||
self.assertTrue(transaction.is_dirty())
|
||||
with self.assertRaises(IntegrityError):
|
||||
|
|
|
@ -576,7 +576,6 @@ class DatabaseConnectionHandlingTests(TransactionTestCase):
|
|||
# Make sure there is an open connection
|
||||
connection.cursor()
|
||||
connection.enter_transaction_management()
|
||||
connection.managed(True)
|
||||
signals.request_finished.send(sender=response._handler_class)
|
||||
self.assertEqual(len(connection.transaction_state), 0)
|
||||
|
||||
|
@ -585,7 +584,6 @@ class DatabaseConnectionHandlingTests(TransactionTestCase):
|
|||
connection.settings_dict['CONN_MAX_AGE'] = 0
|
||||
|
||||
connection.enter_transaction_management()
|
||||
connection.managed(True)
|
||||
connection.set_dirty()
|
||||
# Test that the rollback doesn't succeed (for example network failure
|
||||
# could cause this).
|
||||
|
|
|
@ -25,7 +25,6 @@ class SelectForUpdateTests(TransactionTestCase):
|
|||
|
||||
def setUp(self):
|
||||
transaction.enter_transaction_management()
|
||||
transaction.managed(True)
|
||||
self.person = Person.objects.create(name='Reinhardt')
|
||||
|
||||
# We have to commit here so that code in run_select_for_update can
|
||||
|
@ -37,7 +36,6 @@ class SelectForUpdateTests(TransactionTestCase):
|
|||
new_connections = ConnectionHandler(settings.DATABASES)
|
||||
self.new_connection = new_connections[DEFAULT_DB_ALIAS]
|
||||
self.new_connection.enter_transaction_management()
|
||||
self.new_connection.managed(True)
|
||||
|
||||
# We need to set settings.DEBUG to True so we can capture
|
||||
# the output SQL to examine.
|
||||
|
@ -162,7 +160,6 @@ class SelectForUpdateTests(TransactionTestCase):
|
|||
# We need to enter transaction management again, as this is done on
|
||||
# per-thread basis
|
||||
transaction.enter_transaction_management()
|
||||
transaction.managed(True)
|
||||
people = list(
|
||||
Person.objects.all().select_for_update(nowait=nowait)
|
||||
)
|
||||
|
|
|
@ -268,7 +268,6 @@ class SerializersTransactionTestBase(object):
|
|||
# within a transaction in order to test forward reference
|
||||
# handling.
|
||||
transaction.enter_transaction_management()
|
||||
transaction.managed(True)
|
||||
objs = serializers.deserialize(self.serializer_name, self.fwd_ref_str)
|
||||
with connection.constraint_checks_disabled():
|
||||
for obj in objs:
|
||||
|
|
|
@ -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,12 +1,320 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from django.db import connection, transaction, IntegrityError
|
||||
from django.test import TransactionTestCase, skipUnlessDBFeature
|
||||
from django.utils import six
|
||||
from django.utils.unittest import skipUnless
|
||||
|
||||
from .models import Reporter
|
||||
|
||||
|
||||
class TransactionTests(TransactionTestCase):
|
||||
@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_merged_commit_commit(self):
|
||||
with transaction.atomic():
|
||||
Reporter.objects.create(first_name="Tintin")
|
||||
with transaction.atomic(savepoint=False):
|
||||
Reporter.objects.create(first_name="Archibald", last_name="Haddock")
|
||||
self.assertQuerysetEqual(Reporter.objects.all(),
|
||||
['<Reporter: Archibald Haddock>', '<Reporter: Tintin>'])
|
||||
|
||||
def test_merged_commit_rollback(self):
|
||||
with transaction.atomic():
|
||||
Reporter.objects.create(first_name="Tintin")
|
||||
with six.assertRaisesRegex(self, Exception, "Oops"):
|
||||
with transaction.atomic(savepoint=False):
|
||||
Reporter.objects.create(first_name="Haddock")
|
||||
raise Exception("Oops, that's his last name")
|
||||
# Writes in the outer block are rolled back too.
|
||||
self.assertQuerysetEqual(Reporter.objects.all(), [])
|
||||
|
||||
def test_merged_rollback_commit(self):
|
||||
with six.assertRaisesRegex(self, Exception, "Oops"):
|
||||
with transaction.atomic():
|
||||
Reporter.objects.create(last_name="Tintin")
|
||||
with transaction.atomic(savepoint=False):
|
||||
Reporter.objects.create(last_name="Haddock")
|
||||
raise Exception("Oops, that's his first name")
|
||||
self.assertQuerysetEqual(Reporter.objects.all(), [])
|
||||
|
||||
def test_merged_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(savepoint=False):
|
||||
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 AtomicInsideLegacyTransactionManagementTests(AtomicTests):
|
||||
|
||||
def setUp(self):
|
||||
transaction.enter_transaction_management()
|
||||
|
||||
def tearDown(self):
|
||||
# The tests access the database after exercising 'atomic', making the
|
||||
# connection dirty; a rollback is required to make it clean.
|
||||
transaction.rollback()
|
||||
transaction.leave_transaction_management()
|
||||
|
||||
|
||||
@skipUnless(connection.features.uses_savepoints,
|
||||
"'atomic' requires transactions and savepoints.")
|
||||
class AtomicMergeTests(TransactionTestCase):
|
||||
"""Test merging transactions with savepoint=False."""
|
||||
|
||||
def test_merged_outer_rollback(self):
|
||||
with transaction.atomic():
|
||||
Reporter.objects.create(first_name="Tintin")
|
||||
with transaction.atomic(savepoint=False):
|
||||
Reporter.objects.create(first_name="Archibald", last_name="Haddock")
|
||||
with six.assertRaisesRegex(self, Exception, "Oops"):
|
||||
with transaction.atomic(savepoint=False):
|
||||
Reporter.objects.create(first_name="Tournesol")
|
||||
raise Exception("Oops, that's his last name")
|
||||
# It wasn't possible to roll back
|
||||
self.assertEqual(Reporter.objects.count(), 3)
|
||||
# It wasn't possible to roll back
|
||||
self.assertEqual(Reporter.objects.count(), 3)
|
||||
# The outer block must roll back
|
||||
self.assertQuerysetEqual(Reporter.objects.all(), [])
|
||||
|
||||
def test_merged_inner_savepoint_rollback(self):
|
||||
with transaction.atomic():
|
||||
Reporter.objects.create(first_name="Tintin")
|
||||
with transaction.atomic():
|
||||
Reporter.objects.create(first_name="Archibald", last_name="Haddock")
|
||||
with six.assertRaisesRegex(self, Exception, "Oops"):
|
||||
with transaction.atomic(savepoint=False):
|
||||
Reporter.objects.create(first_name="Tournesol")
|
||||
raise Exception("Oops, that's his last name")
|
||||
# It wasn't possible to roll back
|
||||
self.assertEqual(Reporter.objects.count(), 3)
|
||||
# The first block with a savepoint must roll back
|
||||
self.assertEqual(Reporter.objects.count(), 1)
|
||||
self.assertQuerysetEqual(Reporter.objects.all(), ['<Reporter: Tintin>'])
|
||||
|
||||
def test_merged_outer_rollback_after_inner_failure_and_inner_success(self):
|
||||
with transaction.atomic():
|
||||
Reporter.objects.create(first_name="Tintin")
|
||||
# Inner block without a savepoint fails
|
||||
with six.assertRaisesRegex(self, Exception, "Oops"):
|
||||
with transaction.atomic(savepoint=False):
|
||||
Reporter.objects.create(first_name="Haddock")
|
||||
raise Exception("Oops, that's his last name")
|
||||
# It wasn't possible to roll back
|
||||
self.assertEqual(Reporter.objects.count(), 2)
|
||||
# Inner block with a savepoint succeeds
|
||||
with transaction.atomic(savepoint=False):
|
||||
Reporter.objects.create(first_name="Archibald", last_name="Haddock")
|
||||
# It still wasn't possible to roll back
|
||||
self.assertEqual(Reporter.objects.count(), 3)
|
||||
# The outer block must rollback
|
||||
self.assertQuerysetEqual(Reporter.objects.all(), [])
|
||||
|
||||
|
||||
@skipUnless(connection.features.uses_savepoints,
|
||||
"'atomic' requires transactions and savepoints.")
|
||||
class AtomicErrorsTests(TransactionTestCase):
|
||||
|
||||
def test_atomic_requires_autocommit(self):
|
||||
transaction.set_autocommit(False)
|
||||
try:
|
||||
with self.assertRaises(transaction.TransactionManagementError):
|
||||
with transaction.atomic():
|
||||
pass
|
||||
finally:
|
||||
transaction.set_autocommit(True)
|
||||
|
||||
def test_atomic_prevents_disabling_autocommit(self):
|
||||
autocommit = transaction.get_autocommit()
|
||||
with transaction.atomic():
|
||||
with self.assertRaises(transaction.TransactionManagementError):
|
||||
transaction.set_autocommit(not autocommit)
|
||||
# Make sure autocommit wasn't changed.
|
||||
self.assertEqual(connection.autocommit, autocommit)
|
||||
|
||||
def test_atomic_prevents_calling_transaction_methods(self):
|
||||
with transaction.atomic():
|
||||
with self.assertRaises(transaction.TransactionManagementError):
|
||||
transaction.commit()
|
||||
with self.assertRaises(transaction.TransactionManagementError):
|
||||
transaction.rollback()
|
||||
|
||||
def test_atomic_prevents_calling_transaction_management_methods(self):
|
||||
with transaction.atomic():
|
||||
with self.assertRaises(transaction.TransactionManagementError):
|
||||
transaction.enter_transaction_management()
|
||||
with self.assertRaises(transaction.TransactionManagementError):
|
||||
transaction.leave_transaction_management()
|
||||
|
||||
|
||||
class IgnorePendingDeprecationWarningsMixin(object):
|
||||
|
||||
def setUp(self):
|
||||
super(IgnorePendingDeprecationWarningsMixin, self).setUp()
|
||||
self.catch_warnings = warnings.catch_warnings()
|
||||
self.catch_warnings.__enter__()
|
||||
warnings.filterwarnings("ignore", category=PendingDeprecationWarning)
|
||||
|
||||
def tearDown(self):
|
||||
self.catch_warnings.__exit__(*sys.exc_info())
|
||||
super(IgnorePendingDeprecationWarningsMixin, self).tearDown()
|
||||
|
||||
|
||||
class TransactionTests(IgnorePendingDeprecationWarningsMixin, TransactionTestCase):
|
||||
|
||||
def create_a_reporter_then_fail(self, first, last):
|
||||
a = Reporter(first_name=first, last_name=last)
|
||||
a.save()
|
||||
|
@ -161,7 +469,7 @@ class TransactionTests(TransactionTestCase):
|
|||
)
|
||||
|
||||
|
||||
class TransactionRollbackTests(TransactionTestCase):
|
||||
class TransactionRollbackTests(IgnorePendingDeprecationWarningsMixin, TransactionTestCase):
|
||||
def execute_bad_sql(self):
|
||||
cursor = connection.cursor()
|
||||
cursor.execute("INSERT INTO transactions_reporter (first_name, last_name) VALUES ('Douglas', 'Adams');")
|
||||
|
@ -178,7 +486,7 @@ class TransactionRollbackTests(TransactionTestCase):
|
|||
self.assertRaises(IntegrityError, execute_bad_sql)
|
||||
transaction.rollback()
|
||||
|
||||
class TransactionContextManagerTests(TransactionTestCase):
|
||||
class TransactionContextManagerTests(IgnorePendingDeprecationWarningsMixin, TransactionTestCase):
|
||||
def create_reporter_and_fail(self):
|
||||
Reporter.objects.create(first_name="Bob", last_name="Holtzman")
|
||||
raise Exception
|
||||
|
|
|
@ -6,10 +6,12 @@ from django.test import TransactionTestCase, skipUnlessDBFeature
|
|||
from django.test.utils import override_settings
|
||||
from django.utils.unittest import skipIf, skipUnless
|
||||
|
||||
from transactions.tests import IgnorePendingDeprecationWarningsMixin
|
||||
|
||||
from .models import Mod, M2mA, M2mB
|
||||
|
||||
|
||||
class TestTransactionClosing(TransactionTestCase):
|
||||
class TestTransactionClosing(IgnorePendingDeprecationWarningsMixin, TransactionTestCase):
|
||||
"""
|
||||
Tests to make sure that transactions are properly closed
|
||||
when they should be, and aren't left pending after operations
|
||||
|
@ -166,17 +168,13 @@ class TestTransactionClosing(TransactionTestCase):
|
|||
(connection.settings_dict['NAME'] == ':memory:' or
|
||||
not connection.settings_dict['NAME']),
|
||||
'Test uses multiple connections, but in-memory sqlite does not support this')
|
||||
class TestNewConnection(TransactionTestCase):
|
||||
class TestNewConnection(IgnorePendingDeprecationWarningsMixin, TransactionTestCase):
|
||||
"""
|
||||
Check that new connections don't have special behaviour.
|
||||
"""
|
||||
def setUp(self):
|
||||
self._old_backend = connections[DEFAULT_DB_ALIAS]
|
||||
settings = self._old_backend.settings_dict.copy()
|
||||
opts = settings['OPTIONS'].copy()
|
||||
if 'autocommit' in opts:
|
||||
opts['autocommit'] = False
|
||||
settings['OPTIONS'] = opts
|
||||
new_backend = self._old_backend.__class__(settings, DEFAULT_DB_ALIAS)
|
||||
connections[DEFAULT_DB_ALIAS] = new_backend
|
||||
|
||||
|
@ -193,16 +191,20 @@ class TestNewConnection(TransactionTestCase):
|
|||
"""
|
||||
Users are allowed to commit and rollback connections.
|
||||
"""
|
||||
# The starting value is False, not None.
|
||||
self.assertIs(connection._dirty, False)
|
||||
list(Mod.objects.all())
|
||||
self.assertTrue(connection.is_dirty())
|
||||
connection.commit()
|
||||
self.assertFalse(connection.is_dirty())
|
||||
list(Mod.objects.all())
|
||||
self.assertTrue(connection.is_dirty())
|
||||
connection.rollback()
|
||||
self.assertFalse(connection.is_dirty())
|
||||
connection.set_autocommit(False)
|
||||
try:
|
||||
# The starting value is False, not None.
|
||||
self.assertIs(connection._dirty, False)
|
||||
list(Mod.objects.all())
|
||||
self.assertTrue(connection.is_dirty())
|
||||
connection.commit()
|
||||
self.assertFalse(connection.is_dirty())
|
||||
list(Mod.objects.all())
|
||||
self.assertTrue(connection.is_dirty())
|
||||
connection.rollback()
|
||||
self.assertFalse(connection.is_dirty())
|
||||
finally:
|
||||
connection.set_autocommit(True)
|
||||
|
||||
def test_enter_exit_management(self):
|
||||
orig_dirty = connection._dirty
|
||||
|
@ -210,39 +212,10 @@ class TestNewConnection(TransactionTestCase):
|
|||
connection.leave_transaction_management()
|
||||
self.assertEqual(orig_dirty, connection._dirty)
|
||||
|
||||
def test_commit_unless_managed(self):
|
||||
cursor = connection.cursor()
|
||||
cursor.execute("INSERT into transactions_regress_mod (fld) values (2)")
|
||||
connection.commit_unless_managed()
|
||||
self.assertFalse(connection.is_dirty())
|
||||
self.assertEqual(len(Mod.objects.all()), 1)
|
||||
self.assertTrue(connection.is_dirty())
|
||||
connection.commit_unless_managed()
|
||||
self.assertFalse(connection.is_dirty())
|
||||
|
||||
def test_commit_unless_managed_in_managed(self):
|
||||
cursor = connection.cursor()
|
||||
connection.enter_transaction_management()
|
||||
transaction.managed(True)
|
||||
cursor.execute("INSERT into transactions_regress_mod (fld) values (2)")
|
||||
connection.commit_unless_managed()
|
||||
self.assertTrue(connection.is_dirty())
|
||||
connection.rollback()
|
||||
self.assertFalse(connection.is_dirty())
|
||||
self.assertEqual(len(Mod.objects.all()), 0)
|
||||
connection.commit()
|
||||
connection.leave_transaction_management()
|
||||
self.assertFalse(connection.is_dirty())
|
||||
self.assertEqual(len(Mod.objects.all()), 0)
|
||||
self.assertTrue(connection.is_dirty())
|
||||
connection.commit_unless_managed()
|
||||
self.assertFalse(connection.is_dirty())
|
||||
self.assertEqual(len(Mod.objects.all()), 0)
|
||||
|
||||
|
||||
@skipUnless(connection.vendor == 'postgresql',
|
||||
"This test only valid for PostgreSQL")
|
||||
class TestPostgresAutocommitAndIsolation(TransactionTestCase):
|
||||
class TestPostgresAutocommitAndIsolation(IgnorePendingDeprecationWarningsMixin, TransactionTestCase):
|
||||
"""
|
||||
Tests to make sure psycopg2's autocommit mode and isolation level
|
||||
is restored after entering and leaving transaction management.
|
||||
|
@ -261,7 +234,6 @@ class TestPostgresAutocommitAndIsolation(TransactionTestCase):
|
|||
self._old_backend = connections[DEFAULT_DB_ALIAS]
|
||||
settings = self._old_backend.settings_dict.copy()
|
||||
opts = settings['OPTIONS'].copy()
|
||||
opts['autocommit'] = True
|
||||
opts['isolation_level'] = ISOLATION_LEVEL_SERIALIZABLE
|
||||
settings['OPTIONS'] = opts
|
||||
new_backend = self._old_backend.__class__(settings, DEFAULT_DB_ALIAS)
|
||||
|
@ -275,40 +247,42 @@ class TestPostgresAutocommitAndIsolation(TransactionTestCase):
|
|||
connections[DEFAULT_DB_ALIAS] = self._old_backend
|
||||
|
||||
def test_initial_autocommit_state(self):
|
||||
self.assertTrue(connection.features.uses_autocommit)
|
||||
self.assertEqual(connection.isolation_level, self._autocommit)
|
||||
# Autocommit is activated when the connection is created.
|
||||
connection.cursor().close()
|
||||
self.assertTrue(connection.autocommit)
|
||||
|
||||
def test_transaction_management(self):
|
||||
transaction.enter_transaction_management()
|
||||
transaction.managed(True)
|
||||
self.assertFalse(connection.autocommit)
|
||||
self.assertEqual(connection.isolation_level, self._serializable)
|
||||
|
||||
transaction.leave_transaction_management()
|
||||
self.assertEqual(connection.isolation_level, self._autocommit)
|
||||
self.assertTrue(connection.autocommit)
|
||||
|
||||
def test_transaction_stacking(self):
|
||||
transaction.enter_transaction_management()
|
||||
transaction.managed(True)
|
||||
self.assertFalse(connection.autocommit)
|
||||
self.assertEqual(connection.isolation_level, self._serializable)
|
||||
|
||||
transaction.enter_transaction_management()
|
||||
self.assertFalse(connection.autocommit)
|
||||
self.assertEqual(connection.isolation_level, self._serializable)
|
||||
|
||||
transaction.leave_transaction_management()
|
||||
self.assertFalse(connection.autocommit)
|
||||
self.assertEqual(connection.isolation_level, self._serializable)
|
||||
|
||||
transaction.leave_transaction_management()
|
||||
self.assertEqual(connection.isolation_level, self._autocommit)
|
||||
self.assertTrue(connection.autocommit)
|
||||
|
||||
def test_enter_autocommit(self):
|
||||
transaction.enter_transaction_management()
|
||||
transaction.managed(True)
|
||||
self.assertFalse(connection.autocommit)
|
||||
self.assertEqual(connection.isolation_level, self._serializable)
|
||||
list(Mod.objects.all())
|
||||
self.assertTrue(transaction.is_dirty())
|
||||
# Enter autocommit mode again.
|
||||
transaction.enter_transaction_management(False)
|
||||
transaction.managed(False)
|
||||
self.assertFalse(transaction.is_dirty())
|
||||
self.assertEqual(
|
||||
connection.connection.get_transaction_status(),
|
||||
|
@ -316,12 +290,13 @@ class TestPostgresAutocommitAndIsolation(TransactionTestCase):
|
|||
list(Mod.objects.all())
|
||||
self.assertFalse(transaction.is_dirty())
|
||||
transaction.leave_transaction_management()
|
||||
self.assertFalse(connection.autocommit)
|
||||
self.assertEqual(connection.isolation_level, self._serializable)
|
||||
transaction.leave_transaction_management()
|
||||
self.assertEqual(connection.isolation_level, self._autocommit)
|
||||
self.assertTrue(connection.autocommit)
|
||||
|
||||
|
||||
class TestManyToManyAddTransaction(TransactionTestCase):
|
||||
class TestManyToManyAddTransaction(IgnorePendingDeprecationWarningsMixin, TransactionTestCase):
|
||||
def test_manyrelated_add_commit(self):
|
||||
"Test for https://code.djangoproject.com/ticket/16818"
|
||||
a = M2mA.objects.create()
|
||||
|
@ -336,8 +311,10 @@ class TestManyToManyAddTransaction(TransactionTestCase):
|
|||
self.assertEqual(a.others.count(), 1)
|
||||
|
||||
|
||||
class SavepointTest(TransactionTestCase):
|
||||
class SavepointTest(IgnorePendingDeprecationWarningsMixin, TransactionTestCase):
|
||||
|
||||
@skipIf(connection.vendor == 'sqlite',
|
||||
"SQLite doesn't support savepoints in managed mode")
|
||||
@skipUnlessDBFeature('uses_savepoints')
|
||||
def test_savepoint_commit(self):
|
||||
@commit_manually
|
||||
|
@ -353,6 +330,8 @@ class SavepointTest(TransactionTestCase):
|
|||
|
||||
work()
|
||||
|
||||
@skipIf(connection.vendor == 'sqlite',
|
||||
"SQLite doesn't support savepoints in managed mode")
|
||||
@skipIf(connection.vendor == 'mysql' and
|
||||
connection.features._mysql_storage_engine == 'MyISAM',
|
||||
"MyISAM MySQL storage engine doesn't support savepoints")
|
||||
|
|
Loading…
Reference in New Issue