Deprecated transaction.commit/rollback_unless_managed.

Since "unless managed" now means "if database-level autocommit",
committing or rolling back doesn't have any effect.

Restored transactional integrity in a few places that relied on
automatically-started transactions with a transitory API.
This commit is contained in:
Aymeric Augustin 2013-03-04 13:12:59 +01:00
parent 14aa563f51
commit ba5138b1c0
17 changed files with 116 additions and 208 deletions

View File

@ -555,10 +555,6 @@ class LayerMapping(object):
except SystemExit: except SystemExit:
raise raise
except Exception as msg: 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: if strict:
# Bailing out if the `strict` keyword is set. # Bailing out if the `strict` keyword is set.
if not silent: if not silent:

View File

@ -74,7 +74,6 @@ class SessionStore(SessionBase):
@classmethod @classmethod
def clear_expired(cls): def clear_expired(cls):
Session.objects.filter(expire_date__lt=timezone.now()).delete() Session.objects.filter(expire_date__lt=timezone.now()).delete()
transaction.commit_unless_managed()
# At bottom to avoid circular import # At bottom to avoid circular import

View File

@ -10,7 +10,7 @@ except ImportError:
from django.conf import settings from django.conf import settings
from django.core.cache.backends.base import BaseCache 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 import timezone, six
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
@ -70,7 +70,6 @@ class DatabaseCache(BaseDatabaseCache):
cursor = connections[db].cursor() cursor = connections[db].cursor()
cursor.execute("DELETE FROM %s " cursor.execute("DELETE FROM %s "
"WHERE cache_key = %%s" % table, [key]) "WHERE cache_key = %%s" % table, [key])
transaction.commit_unless_managed(using=db)
return default return default
value = connections[db].ops.process_clob(row[1]) value = connections[db].ops.process_clob(row[1])
return pickle.loads(base64.b64decode(force_bytes(value))) 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)]) [key, b64encoded, connections[db].ops.value_to_db_datetime(exp)])
except DatabaseError: except DatabaseError:
# To be threadsafe, updates/inserts are allowed to fail silently # To be threadsafe, updates/inserts are allowed to fail silently
transaction.rollback_unless_managed(using=db)
return False return False
else: else:
transaction.commit_unless_managed(using=db)
return True return True
def delete(self, key, version=None): def delete(self, key, version=None):
@ -139,7 +136,6 @@ class DatabaseCache(BaseDatabaseCache):
cursor = connections[db].cursor() cursor = connections[db].cursor()
cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % table, [key]) cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % table, [key])
transaction.commit_unless_managed(using=db)
def has_key(self, key, version=None): def has_key(self, key, version=None):
key = self.make_key(key, version=version) key = self.make_key(key, version=version)
@ -184,7 +180,6 @@ class DatabaseCache(BaseDatabaseCache):
table = connections[db].ops.quote_name(self._table) table = connections[db].ops.quote_name(self._table)
cursor = connections[db].cursor() cursor = connections[db].cursor()
cursor.execute('DELETE FROM %s' % table) cursor.execute('DELETE FROM %s' % table)
transaction.commit_unless_managed(using=db)
# For backwards compatibility # For backwards compatibility
class CacheClass(DatabaseCache): class CacheClass(DatabaseCache):

View File

@ -53,14 +53,13 @@ class Command(LabelCommand):
for i, line in enumerate(table_output): for i, line in enumerate(table_output):
full_statement.append(' %s%s' % (line, i < len(table_output)-1 and ',' or '')) full_statement.append(' %s%s' % (line, i < len(table_output)-1 and ',' or ''))
full_statement.append(');') full_statement.append(');')
curs = connection.cursor() with transaction.commit_on_success_unless_managed():
try: curs = connection.cursor()
curs.execute("\n".join(full_statement)) try:
except DatabaseError as e: curs.execute("\n".join(full_statement))
transaction.rollback_unless_managed(using=db) except DatabaseError as e:
raise CommandError( raise CommandError(
"Cache table '%s' could not be created.\nThe error was: %s." % "Cache table '%s' could not be created.\nThe error was: %s." %
(tablename, force_text(e))) (tablename, force_text(e)))
for statement in index_output: for statement in index_output:
curs.execute(statement) curs.execute(statement)
transaction.commit_unless_managed(using=db)

View File

@ -57,18 +57,17 @@ Are you sure you want to do this?
if confirm == 'yes': if confirm == 'yes':
try: try:
cursor = connection.cursor() with transaction.commit_on_success_unless_managed():
for sql in sql_list: cursor = connection.cursor()
cursor.execute(sql) for sql in sql_list:
cursor.execute(sql)
except Exception as e: except Exception as e:
transaction.rollback_unless_managed(using=db)
raise CommandError("""Database %s couldn't be flushed. Possible reasons: raise CommandError("""Database %s couldn't be flushed. Possible reasons:
* The database isn't running or isn't configured correctly. * The database isn't running or isn't configured correctly.
* At least one of the expected database tables doesn't exist. * At least one of the expected database tables doesn't exist.
* The SQL was invalid. * 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. 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)) The full error: %s""" % (connection.settings_dict['NAME'], e))
transaction.commit_unless_managed(using=db)
# Emit the post sync signal. This allows individual # Emit the post sync signal. This allows individual
# applications to respond as if the database had been # applications to respond as if the database had been

View File

@ -73,7 +73,6 @@ class Command(BaseCommand):
# Start transaction management. All fixtures are installed in a # Start transaction management. All fixtures are installed in a
# single transaction to ensure that all references are resolved. # single transaction to ensure that all references are resolved.
if commit: if commit:
transaction.commit_unless_managed(using=self.using)
transaction.enter_transaction_management(using=self.using) transaction.enter_transaction_management(using=self.using)
class SingleZipReader(zipfile.ZipFile): class SingleZipReader(zipfile.ZipFile):

View File

@ -83,26 +83,25 @@ class Command(NoArgsCommand):
# Create the tables for each model # Create the tables for each model
if verbosity >= 1: if verbosity >= 1:
self.stdout.write("Creating tables ...\n") self.stdout.write("Creating tables ...\n")
for app_name, model_list in manifest.items(): with transaction.commit_on_success_unless_managed(using=db):
for model in model_list: for app_name, model_list in manifest.items():
# Create the model's database table, if it doesn't already exist. for model in model_list:
if verbosity >= 3: # Create the model's database table, if it doesn't already exist.
self.stdout.write("Processing %s.%s model\n" % (app_name, model._meta.object_name)) if verbosity >= 3:
sql, references = connection.creation.sql_create_model(model, self.style, seen_models) self.stdout.write("Processing %s.%s model\n" % (app_name, model._meta.object_name))
seen_models.add(model) sql, references = connection.creation.sql_create_model(model, self.style, seen_models)
created_models.add(model) seen_models.add(model)
for refto, refs in references.items(): created_models.add(model)
pending_references.setdefault(refto, []).extend(refs) for refto, refs in references.items():
if refto in seen_models: pending_references.setdefault(refto, []).extend(refs)
sql.extend(connection.creation.sql_for_pending_references(refto, self.style, pending_references)) if refto in seen_models:
sql.extend(connection.creation.sql_for_pending_references(model, self.style, pending_references)) sql.extend(connection.creation.sql_for_pending_references(refto, self.style, pending_references))
if verbosity >= 1 and sql: sql.extend(connection.creation.sql_for_pending_references(model, self.style, pending_references))
self.stdout.write("Creating table %s\n" % model._meta.db_table) if verbosity >= 1 and sql:
for statement in sql: self.stdout.write("Creating table %s\n" % model._meta.db_table)
cursor.execute(statement) for statement in sql:
tables.append(connection.introspection.table_name_converter(model._meta.db_table)) cursor.execute(statement)
tables.append(connection.introspection.table_name_converter(model._meta.db_table))
transaction.commit_unless_managed(using=db)
# Send the post_syncdb signal, so individual apps can do whatever they need # Send the post_syncdb signal, so individual apps can do whatever they need
# to do at this point. # to do at this point.
@ -122,17 +121,16 @@ class Command(NoArgsCommand):
if custom_sql: if custom_sql:
if verbosity >= 2: if verbosity >= 2:
self.stdout.write("Installing custom SQL for %s.%s model\n" % (app_name, model._meta.object_name)) self.stdout.write("Installing custom SQL for %s.%s model\n" % (app_name, model._meta.object_name))
try: with transaction.commit_on_success_unless_managed(using=db):
for sql in custom_sql: try:
cursor.execute(sql) for sql in custom_sql:
except Exception as e: cursor.execute(sql)
self.stderr.write("Failed to install custom SQL for %s.%s model: %s\n" % \ except Exception as e:
(app_name, model._meta.object_name, e)) self.stderr.write("Failed to install custom SQL for %s.%s model: %s\n" % \
if show_traceback: (app_name, model._meta.object_name, e))
traceback.print_exc() if show_traceback:
transaction.rollback_unless_managed(using=db) traceback.print_exc()
else: raise
transaction.commit_unless_managed(using=db)
else: else:
if verbosity >= 3: if verbosity >= 3:
self.stdout.write("No custom SQL for %s.%s model\n" % (app_name, model._meta.object_name)) 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 index_sql:
if verbosity >= 2: if verbosity >= 2:
self.stdout.write("Installing index for %s.%s model\n" % (app_name, model._meta.object_name)) self.stdout.write("Installing index for %s.%s model\n" % (app_name, model._meta.object_name))
try: with transaction.commit_on_success_unless_managed(using=db):
for sql in index_sql: try:
cursor.execute(sql) for sql in index_sql:
except Exception as e: cursor.execute(sql)
self.stderr.write("Failed to install index for %s.%s model: %s\n" % \ except Exception as e:
(app_name, model._meta.object_name, e)) self.stderr.write("Failed to install index for %s.%s model: %s\n" % \
transaction.rollback_unless_managed(using=db) (app_name, model._meta.object_name, e))
else: raise
transaction.commit_unless_managed(using=db)
# Load initial_data fixtures (unless that has been disabled) # Load initial_data fixtures (unless that has been disabled)
if load_initial_data: if load_initial_data:

View File

@ -77,14 +77,3 @@ def close_old_connections(**kwargs):
conn.close_if_unusable_or_obsolete() conn.close_if_unusable_or_obsolete()
signals.request_started.connect(close_old_connections) signals.request_started.connect(close_old_connections)
signals.request_finished.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)

View File

@ -339,27 +339,6 @@ class BaseDatabaseWrapper(object):
return self.transaction_state[-1] return self.transaction_state[-1]
return settings.TRANSACTIONS_MANAGED return settings.TRANSACTIONS_MANAGED
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 ##### ##### Foreign key constraints checks handling #####
@contextmanager @contextmanager

View File

@ -58,8 +58,6 @@ class DatabaseWrapper(BaseDatabaseWrapper):
_set_autocommit = complain _set_autocommit = complain
set_dirty = complain set_dirty = complain
set_clean = complain set_clean = complain
commit_unless_managed = complain
rollback_unless_managed = ignore
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(DatabaseWrapper, self).__init__(*args, **kwargs) super(DatabaseWrapper, self).__init__(*args, **kwargs)

View File

@ -609,48 +609,48 @@ class Model(six.with_metaclass(ModelBase)):
if update_fields: if update_fields:
non_pks = [f for f in non_pks if f.name in update_fields or f.attname in 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. with transaction.commit_on_success_unless_managed(using=using):
pk_val = self._get_pk_val(meta) # First, try an UPDATE. If that doesn't update anything, do an INSERT.
pk_set = pk_val is not None pk_val = self._get_pk_val(meta)
record_exists = True pk_set = pk_val is not None
manager = cls._base_manager record_exists = True
if pk_set: manager = cls._base_manager
# Determine if we should do an update (pk already exists, forced update, if pk_set:
# no force_insert) # Determine if we should do an update (pk already exists, forced update,
if ((force_update or update_fields) or (not force_insert and # no force_insert)
manager.using(using).filter(pk=pk_val).exists())): if ((force_update or update_fields) or (not force_insert and
if force_update or non_pks: manager.using(using).filter(pk=pk_val).exists())):
values = [(f, None, (raw and getattr(self, f.attname) or f.pre_save(self, False))) for f in non_pks] if force_update or non_pks:
if values: values = [(f, None, (raw and getattr(self, f.attname) or f.pre_save(self, False))) for f in non_pks]
rows = manager.using(using).filter(pk=pk_val)._update(values) if values:
if force_update and not rows: rows = manager.using(using).filter(pk=pk_val)._update(values)
raise DatabaseError("Forced update did not affect any rows.") if force_update and not rows:
if update_fields and not rows: raise DatabaseError("Forced update did not affect any rows.")
raise DatabaseError("Save with update_fields did not affect any rows.") if update_fields and not rows:
else: 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 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 update_pk = bool(meta.has_auto_field and not pk_set)
if not pk_set: result = manager._insert([self], fields=fields, return_id=update_pk, using=using, raw=raw)
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 update_pk:
setattr(self, meta.pk.attname, result)
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)
# Store the database on which the object was saved # Store the database on which the object was saved
self._state.db = using 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 order_name = ordered_obj._meta.order_with_respect_to.name
# FIXME: It would be nice if there was an "update many" version of update # FIXME: It would be nice if there was an "update many" version of update
# for situations like this. # for situations like this.
for i, j in enumerate(id_list): with transaction.commit_on_success_unless_managed(using=using):
ordered_obj.objects.filter(**{'pk': j, order_name: rel_val}).update(_order=i) for i, j in enumerate(id_list):
transaction.commit_unless_managed(using=using) ordered_obj.objects.filter(**{'pk': j, order_name: rel_val}).update(_order=i)
def method_get_order(ordered_obj, self): def method_get_order(ordered_obj, self):

View File

@ -62,8 +62,6 @@ def force_managed(func):
func(self, *args, **kwargs) func(self, *args, **kwargs)
if forced_managed: if forced_managed:
transaction.commit(using=self.using) transaction.commit(using=self.using)
else:
transaction.commit_unless_managed(using=self.using)
finally: finally:
if forced_managed: if forced_managed:
transaction.leave_transaction_management(using=self.using) transaction.leave_transaction_management(using=self.using)

View File

@ -460,8 +460,6 @@ class QuerySet(object):
self._batched_insert(objs_without_pk, fields, batch_size) self._batched_insert(objs_without_pk, fields, batch_size)
if forced_managed: if forced_managed:
transaction.commit(using=self.db) transaction.commit(using=self.db)
else:
transaction.commit_unless_managed(using=self.db)
finally: finally:
if forced_managed: if forced_managed:
transaction.leave_transaction_management(using=self.db) transaction.leave_transaction_management(using=self.db)
@ -590,8 +588,6 @@ class QuerySet(object):
rows = query.get_compiler(self.db).execute_sql(None) rows = query.get_compiler(self.db).execute_sql(None)
if forced_managed: if forced_managed:
transaction.commit(using=self.db) transaction.commit(using=self.db)
else:
transaction.commit_unless_managed(using=self.db)
finally: finally:
if forced_managed: if forced_managed:
transaction.leave_transaction_management(using=self.db) transaction.leave_transaction_management(using=self.db)

View File

@ -123,16 +123,12 @@ def managed(flag=True, using=None):
PendingDeprecationWarning, stacklevel=2) PendingDeprecationWarning, stacklevel=2)
def commit_unless_managed(using=None): def commit_unless_managed(using=None):
""" warnings.warn("'commit_unless_managed' is now a no-op.",
Commits changes if the system is not in managed transaction mode. PendingDeprecationWarning, stacklevel=2)
"""
get_connection(using).commit_unless_managed()
def rollback_unless_managed(using=None): def rollback_unless_managed(using=None):
""" warnings.warn("'rollback_unless_managed' is now a no-op.",
Rolls back changes if the system is not in managed transaction mode. PendingDeprecationWarning, stacklevel=2)
"""
get_connection(using).rollback_unless_managed()
############### ###############
# Public APIs # # Public APIs #
@ -280,3 +276,18 @@ def commit_manually(using=None):
leave_transaction_management(using=using) leave_transaction_management(using=using)
return _transaction_func(entering, exiting, using) return _transaction_func(entering, exiting, using)
def commit_on_success_unless_managed(using=None):
"""
Transitory API to preserve backwards-compatibility while refactoring.
"""
if is_managed(using):
def entering(using):
pass
def exiting(exc_value, using):
set_dirty(using=using)
return _transaction_func(entering, exiting, using)
else:
return commit_on_success(using)

View File

@ -157,14 +157,6 @@ class DocTestRunner(doctest.DocTestRunner):
doctest.DocTestRunner.__init__(self, *args, **kwargs) doctest.DocTestRunner.__init__(self, *args, **kwargs)
self.optionflags = doctest.ELLIPSIS 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): class _AssertNumQueriesContext(CaptureQueriesContext):
def __init__(self, test_case, num, connection): def __init__(self, test_case, num, connection):
@ -490,14 +482,10 @@ class TransactionTestCase(SimpleTestCase):
conn.ops.sequence_reset_by_name_sql(no_style(), conn.ops.sequence_reset_by_name_sql(no_style(),
conn.introspection.sequence_list()) conn.introspection.sequence_list())
if sql_list: if sql_list:
try: with transaction.commit_on_success_unless_managed(using=db_name):
cursor = conn.cursor() cursor = conn.cursor()
for sql in sql_list: for sql in sql_list:
cursor.execute(sql) cursor.execute(sql)
except Exception:
transaction.rollback_unless_managed(using=db_name)
raise
transaction.commit_unless_managed(using=db_name)
def _fixture_setup(self): def _fixture_setup(self):
for db_name in self._databases_names(include_mirrors=False): for db_name in self._databases_names(include_mirrors=False):
@ -537,11 +525,6 @@ class TransactionTestCase(SimpleTestCase):
conn.close() conn.close()
def _fixture_teardown(self): 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): for db in self._databases_names(include_mirrors=False):
call_command('flush', verbosity=0, interactive=False, database=db, call_command('flush', verbosity=0, interactive=False, database=db,
skip_validation=True, reset_sequences=False) skip_validation=True, reset_sequences=False)

View File

@ -352,6 +352,8 @@ these changes.
- ``django.db.close_connection()`` - ``django.db.close_connection()``
- ``django.db.backends.creation.BaseDatabaseCreation.set_autocommit()`` - ``django.db.backends.creation.BaseDatabaseCreation.set_autocommit()``
- ``django.db.transaction.managed()`` - ``django.db.transaction.managed()``
- ``django.db.transaction.commit_unless_managed()``
- ``django.db.transaction.rollback_unless_managed()``
2.0 2.0
--- ---

View File

@ -208,38 +208,6 @@ class TestNewConnection(TransactionTestCase):
connection.leave_transaction_management() connection.leave_transaction_management()
self.assertEqual(orig_dirty, connection._dirty) self.assertEqual(orig_dirty, connection._dirty)
# TODO: update this test to account for database-level autocommit.
@expectedFailure
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())
# TODO: update this test to account for database-level autocommit.
@expectedFailure
def test_commit_unless_managed_in_managed(self):
cursor = connection.cursor()
connection.enter_transaction_management()
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', @skipUnless(connection.vendor == 'postgresql',
"This test only valid for PostgreSQL") "This test only valid for PostgreSQL")