Separated autocommit and isolation level handling for PostgreSQL.

Autocommit cannot be manipulated independently from an open connection.
This commit introduces a minor change in behavior: entering transaction
management forces opening a databasse connection. This shouldn't be
backwards incompatible in any practical use case.
This commit is contained in:
Aymeric Augustin 2013-03-02 16:57:56 +01:00
parent f515619494
commit 8717b0668c
2 changed files with 53 additions and 40 deletions

View File

@ -77,21 +77,21 @@ 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)
self.introspection = DatabaseIntrospection(self)
self.validation = BaseDatabaseValidation(self)
autocommit = opts.get('autocommit', False)
self.features.uses_autocommit = autocommit
self.features.uses_savepoints = not autocommit
def get_connection_params(self):
settings_dict = self.settings_dict
if not settings_dict['NAME']:
@ -135,11 +135,12 @@ 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)
if self.features.uses_autocommit:
self.set_autocommit(True)
def create_cursor(self):
cursor = self.connection.cursor()
@ -172,42 +173,40 @@ class DatabaseWrapper(BaseDatabaseWrapper):
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)
if self.connection is None: # Force creating a connection.
self.cursor().close()
if self.features.uses_autocommit and managed and self.autocommit:
self.set_autocommit(False)
self.features.uses_savepoints = True
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)
if self.connection is None: # Force creating a connection.
self.cursor().close()
if self.features.uses_autocommit and not managed and not self.autocommit:
self.rollback() # Must terminate transaction first.
self.set_autocommit(True)
self.features.uses_savepoints = False
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_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 _set_autocommit(self, autocommit):
if autocommit:
level = psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT
if self.psycopg2_version >= (2, 4, 2):
self.connection.autocommit = autocommit
else:
level = self.settings_dict["OPTIONS"].get('isolation_level',
psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED)
self._set_isolation_level(level)
if autocommit:
level = psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT
else:
level = self.isolation_level
self.connection.set_isolation_level(level)
def set_dirty(self):
if ((self.transaction_state and self.transaction_state[-1]) or
@ -231,6 +230,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():

View File

@ -274,31 +274,39 @@ class TestPostgresAutocommitAndIsolation(TransactionTestCase):
connections[DEFAULT_DB_ALIAS] = self._old_backend
def test_initial_autocommit_state(self):
# Autocommit is activated when the connection is created.
connection.cursor().close()
self.assertTrue(connection.features.uses_autocommit)
self.assertEqual(connection.isolation_level, self._autocommit)
self.assertTrue(connection.autocommit)
def test_transaction_management(self):
transaction.enter_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_transaction_stacking(self):
transaction.enter_transaction_management()
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()
self.assertFalse(connection.autocommit)
self.assertEqual(connection.isolation_level, self._serializable)
list(Mod.objects.all())
self.assertTrue(transaction.is_dirty())
@ -311,9 +319,10 @@ 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):