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): def __init__(self, *args, **kwargs):
super(DatabaseWrapper, self).__init__(*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) 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.ops = DatabaseOperations(self)
self.client = DatabaseClient(self) self.client = DatabaseClient(self)
self.creation = DatabaseCreation(self) self.creation = DatabaseCreation(self)
self.introspection = DatabaseIntrospection(self) self.introspection = DatabaseIntrospection(self)
self.validation = BaseDatabaseValidation(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): def get_connection_params(self):
settings_dict = self.settings_dict settings_dict = self.settings_dict
if not settings_dict['NAME']: if not settings_dict['NAME']:
@ -135,11 +135,12 @@ class DatabaseWrapper(BaseDatabaseWrapper):
if conn_tz != tz: if conn_tz != tz:
# Set the time zone in autocommit mode (see #17062) # Set the time zone in autocommit mode (see #17062)
self.connection.set_isolation_level( self.set_autocommit(True)
psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
self.connection.cursor().execute( self.connection.cursor().execute(
self.ops.set_time_zone_sql(), [tz]) self.ops.set_time_zone_sql(), [tz])
self.connection.set_isolation_level(self.isolation_level) self.connection.set_isolation_level(self.isolation_level)
if self.features.uses_autocommit:
self.set_autocommit(True)
def create_cursor(self): def create_cursor(self):
cursor = self.connection.cursor() cursor = self.connection.cursor()
@ -172,42 +173,40 @@ class DatabaseWrapper(BaseDatabaseWrapper):
Switch the isolation level when needing transaction support, so that Switch the isolation level when needing transaction support, so that
the same transaction is visible across all the queries. the same transaction is visible across all the queries.
""" """
if self.features.uses_autocommit and managed and not self.isolation_level: if self.connection is None: # Force creating a connection.
level = self.settings_dict["OPTIONS"].get('isolation_level', self.cursor().close()
psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED) if self.features.uses_autocommit and managed and self.autocommit:
self._set_isolation_level(level) self.set_autocommit(False)
self.features.uses_savepoints = True
def _leave_transaction_management(self, managed): def _leave_transaction_management(self, managed):
""" """
If the normal operating mode is "autocommit", switch back to that when If the normal operating mode is "autocommit", switch back to that when
leaving transaction management. leaving transaction management.
""" """
if self.features.uses_autocommit and not managed and self.isolation_level: if self.connection is None: # Force creating a connection.
self._set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) 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): def _set_isolation_level(self, isolation_level):
""" assert isolation_level in range(1, 5) # Use set_autocommit for level = 0
Do all the related feature configurations for changing isolation if self.psycopg2_version >= (2, 4, 2):
levels. This doesn't touch the uses_autocommit feature, since that self.connection.set_session(isolation_level=isolation_level)
controls the movement *between* isolation levels. else:
""" self.connection.set_isolation_level(isolation_level)
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_autocommit(self, autocommit): def _set_autocommit(self, autocommit):
if autocommit: if self.psycopg2_version >= (2, 4, 2):
level = psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT self.connection.autocommit = autocommit
else: else:
level = self.settings_dict["OPTIONS"].get('isolation_level', if autocommit:
psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED) level = psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT
self._set_isolation_level(level) else:
level = self.isolation_level
self.connection.set_isolation_level(level)
def set_dirty(self): def set_dirty(self):
if ((self.transaction_state and self.transaction_state[-1]) or if ((self.transaction_state and self.transaction_state[-1]) or
@ -231,6 +230,11 @@ class DatabaseWrapper(BaseDatabaseWrapper):
else: else:
return True 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 @cached_property
def pg_version(self): def pg_version(self):
with self.temporary_connection(): with self.temporary_connection():

View File

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