Skip to content

Commit 5fb6667

Browse files
committed
Fixed #3460 -- Added an ability to enable true autocommit for psycopg2 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
1 parent 0543f33 commit 5fb6667

File tree

8 files changed

+220
-56
lines changed

8 files changed

+220
-56
lines changed

django/db/backends/__init__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,21 @@ def _rollback(self):
4141
if self.connection is not None:
4242
return self.connection.rollback()
4343

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+
4459
def _savepoint(self, sid):
4560
if not self.features.uses_savepoints:
4661
return
@@ -81,6 +96,8 @@ class BaseDatabaseFeatures(object):
8196
update_can_self_select = True
8297
interprets_empty_strings_as_nulls = False
8398
can_use_chunked_reads = True
99+
can_return_id_from_insert = False
100+
uses_autocommit = False
84101
uses_savepoints = False
85102
# If True, don't use integer foreign keys referring to, e.g., positive
86103
# integer primary keys.
@@ -230,6 +247,15 @@ def pk_default_value(self):
230247
"""
231248
return 'DEFAULT'
232249

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+
233259
def query_class(self, DefaultQueryClass):
234260
"""
235261
Given the default Query class, returns a custom Query class

django/db/backends/postgresql_psycopg2/base.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
Requires psycopg 2: http://initd.org/projects/psycopg2
55
"""
66

7+
from django.conf import settings
78
from django.db.backends import *
89
from django.db.backends.postgresql.operations import DatabaseOperations as PostgresqlDatabaseOperations
910
from django.db.backends.postgresql.client import DatabaseClient
@@ -28,7 +29,7 @@
2829

2930
class DatabaseFeatures(BaseDatabaseFeatures):
3031
needs_datetime_string_cast = False
31-
uses_savepoints = True
32+
can_return_id_from_insert = True
3233

3334
class DatabaseOperations(PostgresqlDatabaseOperations):
3435
def last_executed_query(self, cursor, sql, params):
@@ -37,6 +38,9 @@ def last_executed_query(self, cursor, sql, params):
3738
# http://www.initd.org/tracker/psycopg/wiki/psycopg2_documentation#postgresql-status-message-and-executed-query
3839
return cursor.query
3940

41+
def return_insert_id(self):
42+
return "RETURNING %s"
43+
4044
class DatabaseWrapper(BaseDatabaseWrapper):
4145
operators = {
4246
'exact': '= %s',
@@ -57,8 +61,14 @@ class DatabaseWrapper(BaseDatabaseWrapper):
5761

5862
def __init__(self, *args, **kwargs):
5963
super(DatabaseWrapper, self).__init__(*args, **kwargs)
60-
64+
6165
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()
6272
self.ops = DatabaseOperations()
6373
self.client = DatabaseClient(self)
6474
self.creation = DatabaseCreation(self)
@@ -77,6 +87,8 @@ def _cursor(self):
7787
'database': settings_dict['DATABASE_NAME'],
7888
}
7989
conn_params.update(settings_dict['DATABASE_OPTIONS'])
90+
if 'autocommit' in conn_params:
91+
del conn_params['autocommit']
8092
if settings_dict['DATABASE_USER']:
8193
conn_params['user'] = settings_dict['DATABASE_USER']
8294
if settings_dict['DATABASE_PASSWORD']:
@@ -86,7 +98,6 @@ def _cursor(self):
8698
if settings_dict['DATABASE_PORT']:
8799
conn_params['port'] = settings_dict['DATABASE_PORT']
88100
self.connection = Database.connect(**conn_params)
89-
self.connection.set_isolation_level(1) # make transactions transparent to all cursors
90101
self.connection.set_client_encoding('UTF8')
91102
cursor = self.connection.cursor()
92103
cursor.tzinfo_factory = None
@@ -98,3 +109,44 @@ def _cursor(self):
98109
# No savepoint support for earlier version of PostgreSQL.
99110
self.features.uses_savepoints = False
100111
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+

django/db/models/query.py

Lines changed: 71 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -447,8 +447,20 @@ def update(self, **kwargs):
447447
"Cannot update a query once a slice has been taken."
448448
query = self.query.clone(sql.UpdateQuery)
449449
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()
452464
self._result_cache = None
453465
return rows
454466
update.alters_data = True
@@ -962,6 +974,11 @@ def delete_objects(seen_objs):
962974
Iterate through a list of seen classes, and remove any instances that are
963975
referred to.
964976
"""
977+
if not transaction.is_managed():
978+
transaction.enter_transaction_management()
979+
forced_managed = True
980+
else:
981+
forced_managed = False
965982
try:
966983
ordered_classes = seen_objs.keys()
967984
except CyclicDependency:
@@ -972,51 +989,58 @@ def delete_objects(seen_objs):
972989
ordered_classes = seen_objs.unordered_keys()
973990

974991
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()
10201044

10211045

10221046
def insert_query(model, values, return_id=False, raw_values=False):

django/db/models/sql/subqueries.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,9 +302,13 @@ def as_sql(self):
302302
# We don't need quote_name_unless_alias() here, since these are all
303303
# going to be column names (so we can avoid the extra overhead).
304304
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)]
306307
result.append('(%s)' % ', '.join([qn(c) for c in self.columns]))
307308
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)
308312
return ' '.join(result), self.params
309313

310314
def execute_sql(self, return_id=False):

django/db/transaction.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class TransactionManagementError(Exception):
4040
# database commit.
4141
dirty = {}
4242

43-
def enter_transaction_management():
43+
def enter_transaction_management(managed=True):
4444
"""
4545
Enters transaction management for a running thread. It must be balanced with
4646
the appropriate leave_transaction_management call, since the actual state is
@@ -58,13 +58,15 @@ def enter_transaction_management():
5858
state[thread_ident].append(settings.TRANSACTIONS_MANAGED)
5959
if thread_ident not in dirty:
6060
dirty[thread_ident] = False
61+
connection._enter_transaction_management(managed)
6162

6263
def leave_transaction_management():
6364
"""
6465
Leaves transaction management for a running thread. A dirty flag is carried
6566
over to the surrounding block, as a commit will commit all changes, even
6667
those from outside. (Commits are on connection level.)
6768
"""
69+
connection._leave_transaction_management(is_managed())
6870
thread_ident = thread.get_ident()
6971
if thread_ident in state and state[thread_ident]:
7072
del state[thread_ident][-1]
@@ -216,7 +218,7 @@ def autocommit(func):
216218
"""
217219
def _autocommit(*args, **kw):
218220
try:
219-
enter_transaction_management()
221+
enter_transaction_management(managed=False)
220222
managed(False)
221223
return func(*args, **kw)
222224
finally:

0 commit comments

Comments
 (0)