Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #3460 -- Added an ability to enable true autocommit for psycopg…

…2 backend.

Ensure to read the documentation before blindly enabling this: requires some
code audits first, but might well be worth it for busy sites.

Thanks to nicferrier, iamseb and Richard Davies for help with this patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@10029 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 5fb66670367501cab7c50bdf81e2600b840162ee 1 parent 0543f33
Malcolm Tredinnick authored March 11, 2009
26  django/db/backends/__init__.py
@@ -41,6 +41,21 @@ def _rollback(self):
41 41
         if self.connection is not None:
42 42
             return self.connection.rollback()
43 43
 
  44
+    def _enter_transaction_management(self, managed):
  45
+        """
  46
+        A hook for backend-specific changes required when entering manual
  47
+        transaction handling.
  48
+        """
  49
+        pass
  50
+
  51
+    def _leave_transaction_management(self, managed):
  52
+        """
  53
+        A hook for backend-specific changes required when leaving manual
  54
+        transaction handling. Will usually be implemented only when
  55
+        _enter_transaction_management() is also required.
  56
+        """
  57
+        pass
  58
+
44 59
     def _savepoint(self, sid):
45 60
         if not self.features.uses_savepoints:
46 61
             return
@@ -81,6 +96,8 @@ class BaseDatabaseFeatures(object):
81 96
     update_can_self_select = True
82 97
     interprets_empty_strings_as_nulls = False
83 98
     can_use_chunked_reads = True
  99
+    can_return_id_from_insert = False
  100
+    uses_autocommit = False
84 101
     uses_savepoints = False
85 102
     # If True, don't use integer foreign keys referring to, e.g., positive
86 103
     # integer primary keys.
@@ -230,6 +247,15 @@ def pk_default_value(self):
230 247
         """
231 248
         return 'DEFAULT'
232 249
 
  250
+    def return_insert_id(self):
  251
+        """
  252
+        For backends that support returning the last insert ID as part of an
  253
+        insert query, this method returns the SQL to append to the INSERT
  254
+        query. The returned fragment should contain a format string to hold
  255
+        hold the appropriate column.
  256
+        """
  257
+        pass
  258
+
233 259
     def query_class(self, DefaultQueryClass):
234 260
         """
235 261
         Given the default Query class, returns a custom Query class
58  django/db/backends/postgresql_psycopg2/base.py
@@ -4,6 +4,7 @@
4 4
 Requires psycopg 2: http://initd.org/projects/psycopg2
5 5
 """
6 6
 
  7
+from django.conf import settings
7 8
 from django.db.backends import *
8 9
 from django.db.backends.postgresql.operations import DatabaseOperations as PostgresqlDatabaseOperations
9 10
 from django.db.backends.postgresql.client import DatabaseClient
@@ -28,7 +29,7 @@
28 29
 
29 30
 class DatabaseFeatures(BaseDatabaseFeatures):
30 31
     needs_datetime_string_cast = False
31  
-    uses_savepoints = True
  32
+    can_return_id_from_insert = True
32 33
 
33 34
 class DatabaseOperations(PostgresqlDatabaseOperations):
34 35
     def last_executed_query(self, cursor, sql, params):
@@ -37,6 +38,9 @@ def last_executed_query(self, cursor, sql, params):
37 38
         # http://www.initd.org/tracker/psycopg/wiki/psycopg2_documentation#postgresql-status-message-and-executed-query
38 39
         return cursor.query
39 40
 
  41
+    def return_insert_id(self):
  42
+        return "RETURNING %s"
  43
+
40 44
 class DatabaseWrapper(BaseDatabaseWrapper):
41 45
     operators = {
42 46
         'exact': '= %s',
@@ -57,8 +61,14 @@ class DatabaseWrapper(BaseDatabaseWrapper):
57 61
 
58 62
     def __init__(self, *args, **kwargs):
59 63
         super(DatabaseWrapper, self).__init__(*args, **kwargs)
60  
-        
  64
+
61 65
         self.features = DatabaseFeatures()
  66
+        if settings.DATABASE_OPTIONS.get('autocommit', False):
  67
+          self.features.uses_autocommit = True
  68
+          self._iso_level_0()
  69
+        else:
  70
+          self.features.uses_autocommit = False
  71
+          self._iso_level_1()
62 72
         self.ops = DatabaseOperations()
63 73
         self.client = DatabaseClient(self)
64 74
         self.creation = DatabaseCreation(self)
@@ -77,6 +87,8 @@ def _cursor(self):
77 87
                 'database': settings_dict['DATABASE_NAME'],
78 88
             }
79 89
             conn_params.update(settings_dict['DATABASE_OPTIONS'])
  90
+            if 'autocommit' in conn_params:
  91
+                del conn_params['autocommit']
80 92
             if settings_dict['DATABASE_USER']:
81 93
                 conn_params['user'] = settings_dict['DATABASE_USER']
82 94
             if settings_dict['DATABASE_PASSWORD']:
@@ -86,7 +98,6 @@ def _cursor(self):
86 98
             if settings_dict['DATABASE_PORT']:
87 99
                 conn_params['port'] = settings_dict['DATABASE_PORT']
88 100
             self.connection = Database.connect(**conn_params)
89  
-            self.connection.set_isolation_level(1) # make transactions transparent to all cursors
90 101
             self.connection.set_client_encoding('UTF8')
91 102
         cursor = self.connection.cursor()
92 103
         cursor.tzinfo_factory = None
@@ -98,3 +109,44 @@ def _cursor(self):
98 109
                 # No savepoint support for earlier version of PostgreSQL.
99 110
                 self.features.uses_savepoints = False
100 111
         return cursor
  112
+
  113
+    def _enter_transaction_management(self, managed):
  114
+        """
  115
+        Switch the isolation level when needing transaction support, so that
  116
+        the same transaction is visible across all the queries.
  117
+        """
  118
+        if self.features.uses_autocommit and managed and not self.isolation_level:
  119
+            self._iso_level_1()
  120
+
  121
+    def _leave_transaction_management(self, managed):
  122
+        """
  123
+        If the normal operating mode is "autocommit", switch back to that when
  124
+        leaving transaction management.
  125
+        """
  126
+        if self.features.uses_autocommit and not managed and self.isolation_level:
  127
+            self._iso_level_0()
  128
+
  129
+    def _iso_level_0(self):
  130
+        """
  131
+        Do all the related feature configurations for isolation level 0. This
  132
+        doesn't touch the uses_autocommit feature, since that controls the
  133
+        movement *between* isolation levels.
  134
+        """
  135
+        try:
  136
+            if self.connection is not None:
  137
+                self.connection.set_isolation_level(0)
  138
+        finally:
  139
+            self.isolation_level = 0
  140
+            self.features.uses_savepoints = False
  141
+
  142
+    def _iso_level_1(self):
  143
+        """
  144
+        The "isolation level 1" version of _iso_level_0().
  145
+        """
  146
+        try:
  147
+            if self.connection is not None:
  148
+                self.connection.set_isolation_level(1)
  149
+        finally:
  150
+            self.isolation_level = 1
  151
+            self.features.uses_savepoints = True
  152
+
118  django/db/models/query.py
@@ -447,8 +447,20 @@ def update(self, **kwargs):
447 447
                 "Cannot update a query once a slice has been taken."
448 448
         query = self.query.clone(sql.UpdateQuery)
449 449
         query.add_update_values(kwargs)
450  
-        rows = query.execute_sql(None)
451  
-        transaction.commit_unless_managed()
  450
+        if not transaction.is_managed():
  451
+            transaction.enter_transaction_management()
  452
+            forced_managed = True
  453
+        else:
  454
+            forced_managed = False
  455
+        try:
  456
+            rows = query.execute_sql(None)
  457
+            if forced_managed:
  458
+                transaction.commit()
  459
+            else:
  460
+                transaction.commit_unless_managed()
  461
+        finally:
  462
+            if forced_managed:
  463
+                transaction.leave_transaction_management()
452 464
         self._result_cache = None
453 465
         return rows
454 466
     update.alters_data = True
@@ -962,6 +974,11 @@ def delete_objects(seen_objs):
962 974
     Iterate through a list of seen classes, and remove any instances that are
963 975
     referred to.
964 976
     """
  977
+    if not transaction.is_managed():
  978
+        transaction.enter_transaction_management()
  979
+        forced_managed = True
  980
+    else:
  981
+        forced_managed = False
965 982
     try:
966 983
         ordered_classes = seen_objs.keys()
967 984
     except CyclicDependency:
@@ -972,51 +989,58 @@ def delete_objects(seen_objs):
972 989
         ordered_classes = seen_objs.unordered_keys()
973 990
 
974 991
     obj_pairs = {}
975  
-    for cls in ordered_classes:
976  
-        items = seen_objs[cls].items()
977  
-        items.sort()
978  
-        obj_pairs[cls] = items
979  
-
980  
-        # Pre-notify all instances to be deleted.
981  
-        for pk_val, instance in items:
982  
-            signals.pre_delete.send(sender=cls, instance=instance)
983  
-
984  
-        pk_list = [pk for pk,instance in items]
985  
-        del_query = sql.DeleteQuery(cls, connection)
986  
-        del_query.delete_batch_related(pk_list)
987  
-
988  
-        update_query = sql.UpdateQuery(cls, connection)
989  
-        for field, model in cls._meta.get_fields_with_model():
990  
-            if (field.rel and field.null and field.rel.to in seen_objs and
991  
-                    filter(lambda f: f.column == field.column,
992  
-                    field.rel.to._meta.fields)):
993  
-                if model:
994  
-                    sql.UpdateQuery(model, connection).clear_related(field,
995  
-                            pk_list)
996  
-                else:
997  
-                    update_query.clear_related(field, pk_list)
998  
-
999  
-    # Now delete the actual data.
1000  
-    for cls in ordered_classes:
1001  
-        items = obj_pairs[cls]
1002  
-        items.reverse()
1003  
-
1004  
-        pk_list = [pk for pk,instance in items]
1005  
-        del_query = sql.DeleteQuery(cls, connection)
1006  
-        del_query.delete_batch(pk_list)
1007  
-
1008  
-        # Last cleanup; set NULLs where there once was a reference to the
1009  
-        # object, NULL the primary key of the found objects, and perform
1010  
-        # post-notification.
1011  
-        for pk_val, instance in items:
1012  
-            for field in cls._meta.fields:
1013  
-                if field.rel and field.null and field.rel.to in seen_objs:
1014  
-                    setattr(instance, field.attname, None)
1015  
-
1016  
-            signals.post_delete.send(sender=cls, instance=instance)
1017  
-            setattr(instance, cls._meta.pk.attname, None)
1018  
-
1019  
-    transaction.commit_unless_managed()
  992
+    try:
  993
+        for cls in ordered_classes:
  994
+            items = seen_objs[cls].items()
  995
+            items.sort()
  996
+            obj_pairs[cls] = items
  997
+
  998
+            # Pre-notify all instances to be deleted.
  999
+            for pk_val, instance in items:
  1000
+                signals.pre_delete.send(sender=cls, instance=instance)
  1001
+
  1002
+            pk_list = [pk for pk,instance in items]
  1003
+            del_query = sql.DeleteQuery(cls, connection)
  1004
+            del_query.delete_batch_related(pk_list)
  1005
+
  1006
+            update_query = sql.UpdateQuery(cls, connection)
  1007
+            for field, model in cls._meta.get_fields_with_model():
  1008
+                if (field.rel and field.null and field.rel.to in seen_objs and
  1009
+                        filter(lambda f: f.column == field.column,
  1010
+                        field.rel.to._meta.fields)):
  1011
+                    if model:
  1012
+                        sql.UpdateQuery(model, connection).clear_related(field,
  1013
+                                pk_list)
  1014
+                    else:
  1015
+                        update_query.clear_related(field, pk_list)
  1016
+
  1017
+        # Now delete the actual data.
  1018
+        for cls in ordered_classes:
  1019
+            items = obj_pairs[cls]
  1020
+            items.reverse()
  1021
+
  1022
+            pk_list = [pk for pk,instance in items]
  1023
+            del_query = sql.DeleteQuery(cls, connection)
  1024
+            del_query.delete_batch(pk_list)
  1025
+
  1026
+            # Last cleanup; set NULLs where there once was a reference to the
  1027
+            # object, NULL the primary key of the found objects, and perform
  1028
+            # post-notification.
  1029
+            for pk_val, instance in items:
  1030
+                for field in cls._meta.fields:
  1031
+                    if field.rel and field.null and field.rel.to in seen_objs:
  1032
+                        setattr(instance, field.attname, None)
  1033
+
  1034
+                signals.post_delete.send(sender=cls, instance=instance)
  1035
+                setattr(instance, cls._meta.pk.attname, None)
  1036
+
  1037
+        if forced_managed:
  1038
+            transaction.commit()
  1039
+        else:
  1040
+            transaction.commit_unless_managed()
  1041
+    finally:
  1042
+        if forced_managed:
  1043
+            transaction.leave_transaction_management()
1020 1044
 
1021 1045
 
1022 1046
 def insert_query(model, values, return_id=False, raw_values=False):
6  django/db/models/sql/subqueries.py
@@ -302,9 +302,13 @@ def as_sql(self):
302 302
         # We don't need quote_name_unless_alias() here, since these are all
303 303
         # going to be column names (so we can avoid the extra overhead).
304 304
         qn = self.connection.ops.quote_name
305  
-        result = ['INSERT INTO %s' % qn(self.model._meta.db_table)]
  305
+        opts = self.model._meta
  306
+        result = ['INSERT INTO %s' % qn(opts.db_table)]
306 307
         result.append('(%s)' % ', '.join([qn(c) for c in self.columns]))
307 308
         result.append('VALUES (%s)' % ', '.join(self.values))
  309
+        if self.connection.features.can_return_id_from_insert:
  310
+            col = "%s.%s" % (qn(opts.db_table), qn(opts.pk.column))
  311
+            result.append(self.connection.ops.return_insert_id() % col)
308 312
         return ' '.join(result), self.params
309 313
 
310 314
     def execute_sql(self, return_id=False):
6  django/db/transaction.py
@@ -40,7 +40,7 @@ class TransactionManagementError(Exception):
40 40
 # database commit.
41 41
 dirty = {}
42 42
 
43  
-def enter_transaction_management():
  43
+def enter_transaction_management(managed=True):
44 44
     """
45 45
     Enters transaction management for a running thread. It must be balanced with
46 46
     the appropriate leave_transaction_management call, since the actual state is
@@ -58,6 +58,7 @@ def enter_transaction_management():
58 58
         state[thread_ident].append(settings.TRANSACTIONS_MANAGED)
59 59
     if thread_ident not in dirty:
60 60
         dirty[thread_ident] = False
  61
+    connection._enter_transaction_management(managed)
61 62
 
62 63
 def leave_transaction_management():
63 64
     """
@@ -65,6 +66,7 @@ def leave_transaction_management():
65 66
     over to the surrounding block, as a commit will commit all changes, even
66 67
     those from outside. (Commits are on connection level.)
67 68
     """
  69
+    connection._leave_transaction_management(is_managed())
68 70
     thread_ident = thread.get_ident()
69 71
     if thread_ident in state and state[thread_ident]:
70 72
         del state[thread_ident][-1]
@@ -216,7 +218,7 @@ def autocommit(func):
216 218
     """
217 219
     def _autocommit(*args, **kw):
218 220
         try:
219  
-            enter_transaction_management()
  221
+            enter_transaction_management(managed=False)
220 222
             managed(False)
221 223
             return func(*args, **kw)
222 224
         finally:
56  docs/ref/databases.txt
@@ -13,6 +13,8 @@ This file describes some of the features that might be relevant to Django
13 13
 usage. Of course, it is not intended as a replacement for server-specific
14 14
 documentation or reference manuals.
15 15
 
  16
+.. postgresql-notes:
  17
+
16 18
 PostgreSQL notes
17 19
 ================
18 20
 
@@ -29,6 +31,56 @@ aggregate with an database backend falls within the affected release range.
29 31
 .. _known to be faulty: http://archives.postgresql.org/pgsql-bugs/2007-07/msg00046.php
30 32
 .. _Release 8.2.5: http://developer.postgresql.org/pgdocs/postgres/release-8-2-5.html
31 33
 
  34
+Transaction handling
  35
+---------------------
  36
+
  37
+:ref:`By default <topics-db-transactions>`, Django starts a transaction when a
  38
+database connection if first used and commits the result at the end of the
  39
+request/response handling. The PostgreSQL backends normally operate the same
  40
+as any other Django backend in this respect.
  41
+
  42
+Autocommit mode
  43
+~~~~~~~~~~~~~~~
  44
+
  45
+.. versionadded:: 1.1
  46
+
  47
+If your application is particularly read-heavy and doesn't make many database
  48
+writes, the overhead of a constantly open transaction can sometimes be
  49
+noticeable. For those situations, if you're using the ``postgresql_psycopg2``
  50
+backend, you can configure Django to use *"autocommit"* behavior for the
  51
+connection, meaning that each database operation will normally be in its own
  52
+transaction, rather than having the transaction extend over multiple
  53
+operations. In this case, you can still manually start a transaction if you're
  54
+doing something that requires consistency across multiple database operations.
  55
+The autocommit behavior is enabled by setting the ``autocommit`` key in the
  56
+:setting:`DATABASE_OPTIONS` setting::
  57
+
  58
+    DATABASE_OPTIONS = {
  59
+        "autocommit": True,
  60
+    }
  61
+
  62
+In this configuration, Django still ensures that :ref:`delete()
  63
+<topics-db-queries-delete>` and :ref:`update() <topics-db-queries-update>`
  64
+queries run inside a single transaction, so that either all the affected
  65
+objects are changed or none of them are.
  66
+
  67
+.. admonition:: This is database-level autocommit
  68
+
  69
+    This functionality is not the same as the
  70
+    :ref:`topics-db-transactions-autocommit` decorator. That decorator is a
  71
+    Django-level implementation that commits automatically after data changing
  72
+    operations. The feature enabled using the :setting:`DATABASE_OPTIONS`
  73
+    settings provides autocommit behavior at the database adapter level. It
  74
+    commits after *every* operation.
  75
+
  76
+If you are using this feature and performing an operation akin to delete or
  77
+updating that requires multiple operations, you are strongly recommended to
  78
+wrap you operations in manual transaction handling to ensure data consistency.
  79
+You should also audit your existing code for any instances of this behavior
  80
+before enabling this feature. It's faster, but it provides less automatic
  81
+protection for multi-call operations.
  82
+
  83
+
32 84
 .. _mysql-notes:
33 85
 
34 86
 MySQL notes
@@ -199,7 +251,7 @@ Here's a sample configuration which uses a MySQL option file::
199 251
   DATABASE_ENGINE = "mysql"
200 252
   DATABASE_OPTIONS = {
201 253
       'read_default_file': '/path/to/my.cnf',
202  
-      }
  254
+  }
203 255
 
204 256
   # my.cnf
205 257
   [client]
@@ -237,9 +289,7 @@ storage engine, you have a couple of options.
237 289
       creating your tables::
238 290
 
239 291
           DATABASE_OPTIONS = {
240  
-              # ...
241 292
              "init_command": "SET storage_engine=INNODB",
242  
-              # ...
243 293
           }
244 294
 
245 295
       This sets the default storage engine upon connecting to the database.
4  docs/topics/db/queries.txt
@@ -714,6 +714,8 @@ primary key field is called ``name``, these two statements are equivalent::
714 714
     >>> some_obj == other_obj
715 715
     >>> some_obj.name == other_obj.name
716 716
 
  717
+.. _topics-db-queries-delete:
  718
+
717 719
 Deleting objects
718 720
 ================
719 721
 
@@ -756,6 +758,8 @@ complete query set::
756 758
 
757 759
     Entry.objects.all().delete()
758 760
 
  761
+.. _topics-db-queries-update:
  762
+
759 763
 Updating multiple objects at once
760 764
 =================================
761 765
 
2  docs/topics/db/transactions.txt
@@ -63,6 +63,8 @@ particular view function.
63 63
     Although the examples below use view functions as examples, these
64 64
     decorators can be applied to non-view functions as well.
65 65
 
  66
+.. _topics-db-transactions-autocommit:
  67
+
66 68
 ``django.db.transaction.autocommit``
67 69
 ------------------------------------
68 70
 

0 notes on commit 5fb6667

Please sign in to comment.
Something went wrong with that request. Please try again.