From e0449316ebacaa550e9c529f8c9cb9a9b44e3765 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 2 Mar 2013 15:00:28 +0100 Subject: [PATCH] Fixed #18130 -- Made the isolation level configurable on PostgreSQL. Thanks limscoder for the report and niwi for the draft patch. --- .../db/backends/postgresql_psycopg2/base.py | 9 +++-- docs/ref/databases.txt | 35 +++++++++++++++++-- docs/releases/1.6.txt | 2 ++ tests/transactions_regress/tests.py | 24 +++++++------ 4 files changed, 55 insertions(+), 15 deletions(-) diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index d5b6f136961..f9af5073114 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -83,7 +83,8 @@ class DatabaseWrapper(BaseDatabaseWrapper): if autocommit: level = psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT else: - level = psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED + 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) @@ -104,6 +105,8 @@ class DatabaseWrapper(BaseDatabaseWrapper): conn_params.update(settings_dict['OPTIONS']) if 'autocommit' in conn_params: del conn_params['autocommit'] + if 'isolation_level' in conn_params: + del conn_params['isolation_level'] if settings_dict['USER']: conn_params['user'] = settings_dict['USER'] if settings_dict['PASSWORD']: @@ -170,7 +173,9 @@ class DatabaseWrapper(BaseDatabaseWrapper): the same transaction is visible across all the queries. """ if self.features.uses_autocommit and managed and not self.isolation_level: - self._set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED) + level = self.settings_dict["OPTIONS"].get('isolation_level', + psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED) + self._set_isolation_level(level) def _leave_transaction_management(self, managed): """ diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index 34f60e99acd..4e435949a25 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -143,8 +143,11 @@ autocommit behavior is enabled by setting the ``autocommit`` key in the :setting:`OPTIONS` part of your database configuration in :setting:`DATABASES`:: - 'OPTIONS': { - 'autocommit': True, + DATABASES = { + # ... + 'OPTIONS': { + 'autocommit': True, + }, } In this configuration, Django still ensures that :ref:`delete() @@ -168,6 +171,34 @@ 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. +Isolation level +~~~~~~~~~~~~~~~ + +.. versionadded:: 1.6 + +Like PostgreSQL itself, Django defaults to the ``READ COMMITTED`` `isolation +level `_. If you need a higher isolation level +such as ``REPEATABLE READ`` or ``SERIALIZABLE``, set it in the +:setting:`OPTIONS` part of your database configuration in +:setting:`DATABASES`:: + + import psycopg2.extensions + + DATABASES = { + # ... + 'OPTIONS': { + 'isolation_level': psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE, + }, + } + +.. note:: + + Under higher isolation levels, your application should be prepared to + handle exceptions raised on serialization failures. This option is + designed for advanced uses. + +.. _postgresql-isolation-levels: http://www.postgresql.org/docs/devel/static/transaction-iso.html + Indexes for ``varchar`` and ``text`` columns ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 89e7ff17eec..74941a4fb38 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -125,6 +125,8 @@ Minor features * The admin list columns have a ``column-`` class in the HTML so the columns header can be styled with CSS, e.g. to set a column width. +* The isolation level can be customized under PostgreSQL. + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/transactions_regress/tests.py b/tests/transactions_regress/tests.py index 8cc68b7e0d6..6ba04892cd4 100644 --- a/tests/transactions_regress/tests.py +++ b/tests/transactions_regress/tests.py @@ -242,17 +242,18 @@ class TestNewConnection(TransactionTestCase): @skipUnless(connection.vendor == 'postgresql', "This test only valid for PostgreSQL") -class TestPostgresAutocommit(TransactionTestCase): +class TestPostgresAutocommitAndIsolation(TransactionTestCase): """ - Tests to make sure psycopg2's autocommit mode is restored after entering - and leaving transaction management. Refs #16047. + Tests to make sure psycopg2's autocommit mode and isolation level + is restored after entering and leaving transaction management. + Refs #16047, #18130. """ def setUp(self): from psycopg2.extensions import (ISOLATION_LEVEL_AUTOCOMMIT, - ISOLATION_LEVEL_READ_COMMITTED, + ISOLATION_LEVEL_SERIALIZABLE, TRANSACTION_STATUS_IDLE) self._autocommit = ISOLATION_LEVEL_AUTOCOMMIT - self._read_committed = ISOLATION_LEVEL_READ_COMMITTED + self._serializable = ISOLATION_LEVEL_SERIALIZABLE self._idle = TRANSACTION_STATUS_IDLE # We want a clean backend with autocommit = True, so @@ -261,6 +262,7 @@ class TestPostgresAutocommit(TransactionTestCase): 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) connections[DEFAULT_DB_ALIAS] = new_backend @@ -279,7 +281,7 @@ class TestPostgresAutocommit(TransactionTestCase): def test_transaction_management(self): transaction.enter_transaction_management() transaction.managed(True) - self.assertEqual(connection.isolation_level, self._read_committed) + self.assertEqual(connection.isolation_level, self._serializable) transaction.leave_transaction_management() self.assertEqual(connection.isolation_level, self._autocommit) @@ -287,13 +289,13 @@ class TestPostgresAutocommit(TransactionTestCase): def test_transaction_stacking(self): transaction.enter_transaction_management() transaction.managed(True) - self.assertEqual(connection.isolation_level, self._read_committed) + self.assertEqual(connection.isolation_level, self._serializable) transaction.enter_transaction_management() - self.assertEqual(connection.isolation_level, self._read_committed) + self.assertEqual(connection.isolation_level, self._serializable) transaction.leave_transaction_management() - self.assertEqual(connection.isolation_level, self._read_committed) + self.assertEqual(connection.isolation_level, self._serializable) transaction.leave_transaction_management() self.assertEqual(connection.isolation_level, self._autocommit) @@ -301,7 +303,7 @@ class TestPostgresAutocommit(TransactionTestCase): def test_enter_autocommit(self): transaction.enter_transaction_management() transaction.managed(True) - self.assertEqual(connection.isolation_level, self._read_committed) + self.assertEqual(connection.isolation_level, self._serializable) list(Mod.objects.all()) self.assertTrue(transaction.is_dirty()) # Enter autocommit mode again. @@ -314,7 +316,7 @@ class TestPostgresAutocommit(TransactionTestCase): list(Mod.objects.all()) self.assertFalse(transaction.is_dirty()) transaction.leave_transaction_management() - self.assertEqual(connection.isolation_level, self._read_committed) + self.assertEqual(connection.isolation_level, self._serializable) transaction.leave_transaction_management() self.assertEqual(connection.isolation_level, self._autocommit)