mirror of https://github.com/django/django.git
Added savepoint support to the transaction code.
This is a no-op for most databases. Only necessary on PostgreSQL so that we can do things which will possibly intentionally raise an IntegrityError and not have to rollback the entire transaction. Not supported for PostgreSQL versions prior to 8.0, so should be used sparingly in internal Django code. git-svn-id: http://code.djangoproject.com/svn/django/trunk@8314 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
e73bf2bdd9
commit
220993bcc5
|
@ -31,6 +31,21 @@ class BaseDatabaseWrapper(local):
|
||||||
if self.connection is not None:
|
if self.connection is not None:
|
||||||
return self.connection.rollback()
|
return self.connection.rollback()
|
||||||
|
|
||||||
|
def _savepoint(self, sid):
|
||||||
|
if not self.features.uses_savepoints:
|
||||||
|
return
|
||||||
|
self.connection.cursor().execute(self.ops.savepoint_create_sql(sid))
|
||||||
|
|
||||||
|
def _savepoint_rollback(self, sid):
|
||||||
|
if not self.features.uses_savepoints:
|
||||||
|
return
|
||||||
|
self.connection.cursor().execute(self.ops.savepoint_rollback_sql(sid))
|
||||||
|
|
||||||
|
def _savepoint_commit(self, sid):
|
||||||
|
if not self.features.uses_savepoints:
|
||||||
|
return
|
||||||
|
self.connection.cursor().execute(self.ops.savepoint_commit_sql(sid))
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
if self.connection is not None:
|
if self.connection is not None:
|
||||||
self.connection.close()
|
self.connection.close()
|
||||||
|
@ -55,6 +70,7 @@ class BaseDatabaseFeatures(object):
|
||||||
update_can_self_select = True
|
update_can_self_select = True
|
||||||
interprets_empty_strings_as_nulls = False
|
interprets_empty_strings_as_nulls = False
|
||||||
can_use_chunked_reads = True
|
can_use_chunked_reads = True
|
||||||
|
uses_savepoints = False
|
||||||
|
|
||||||
class BaseDatabaseOperations(object):
|
class BaseDatabaseOperations(object):
|
||||||
"""
|
"""
|
||||||
|
@ -226,6 +242,26 @@ class BaseDatabaseOperations(object):
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def savepoint_create_sql(self, sid):
|
||||||
|
"""
|
||||||
|
Returns the SQL for starting a new savepoint. Only required if the
|
||||||
|
"uses_savepoints" feature is True. The "sid" parameter is a string
|
||||||
|
for the savepoint id.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def savepoint_commit_sql(self, sid):
|
||||||
|
"""
|
||||||
|
Returns the SQL for committing the given savepoint.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def savepoint_rollback_sql(self, sid):
|
||||||
|
"""
|
||||||
|
Returns the SQL for rolling back the given savepoint.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def sql_flush(self, style, tables, sequences):
|
def sql_flush(self, style, tables, sequences):
|
||||||
"""
|
"""
|
||||||
Returns a list of SQL statements required to remove all data from
|
Returns a list of SQL statements required to remove all data from
|
||||||
|
@ -394,7 +430,6 @@ class BaseDatabaseIntrospection(object):
|
||||||
|
|
||||||
return sequence_list
|
return sequence_list
|
||||||
|
|
||||||
|
|
||||||
class BaseDatabaseClient(object):
|
class BaseDatabaseClient(object):
|
||||||
"""
|
"""
|
||||||
This class encapsualtes all backend-specific methods for opening a
|
This class encapsualtes all backend-specific methods for opening a
|
||||||
|
|
|
@ -63,6 +63,9 @@ class UnicodeCursorWrapper(object):
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter(self.cursor)
|
return iter(self.cursor)
|
||||||
|
|
||||||
|
class DatabaseFeatures(BaseDatabaseFeatures):
|
||||||
|
uses_savepoints = True
|
||||||
|
|
||||||
class DatabaseWrapper(BaseDatabaseWrapper):
|
class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
operators = {
|
operators = {
|
||||||
'exact': '= %s',
|
'exact': '= %s',
|
||||||
|
@ -84,7 +87,7 @@ 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)
|
||||||
|
|
||||||
self.features = BaseDatabaseFeatures()
|
self.features = DatabaseFeatures()
|
||||||
self.ops = DatabaseOperations()
|
self.ops = DatabaseOperations()
|
||||||
self.client = DatabaseClient()
|
self.client = DatabaseClient()
|
||||||
self.creation = DatabaseCreation(self)
|
self.creation = DatabaseCreation(self)
|
||||||
|
|
|
@ -124,3 +124,13 @@ class DatabaseOperations(BaseDatabaseOperations):
|
||||||
style.SQL_KEYWORD('FROM'),
|
style.SQL_KEYWORD('FROM'),
|
||||||
style.SQL_TABLE(qn(f.m2m_db_table()))))
|
style.SQL_TABLE(qn(f.m2m_db_table()))))
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
def savepoint_create_sql(self, sid):
|
||||||
|
return "SAVEPOINT %s" % sid
|
||||||
|
|
||||||
|
def savepoint_commit_sql(self, sid):
|
||||||
|
return "RELEASE SAVEPOINT %s" % sid
|
||||||
|
|
||||||
|
def savepoint_rollback_sql(self, sid):
|
||||||
|
return "ROLLBACK TO SAVEPOINT %s" % sid
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ psycopg2.extensions.register_adapter(SafeUnicode, psycopg2.extensions.QuotedStri
|
||||||
|
|
||||||
class DatabaseFeatures(BaseDatabaseFeatures):
|
class DatabaseFeatures(BaseDatabaseFeatures):
|
||||||
needs_datetime_string_cast = False
|
needs_datetime_string_cast = False
|
||||||
|
uses_savepoints = True
|
||||||
|
|
||||||
class DatabaseOperations(PostgresqlDatabaseOperations):
|
class DatabaseOperations(PostgresqlDatabaseOperations):
|
||||||
def last_executed_query(self, cursor, sql, params):
|
def last_executed_query(self, cursor, sql, params):
|
||||||
|
|
|
@ -30,9 +30,10 @@ class TransactionManagementError(Exception):
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# The state is a dictionary of lists. The key to the dict is the current
|
# The states are dictionaries of lists. The key to the dict is the current
|
||||||
# thread and the list is handled as a stack of values.
|
# thread and the list is handled as a stack of values.
|
||||||
state = {}
|
state = {}
|
||||||
|
savepoint_state = {}
|
||||||
|
|
||||||
# The dirty flag is set by *_unless_managed functions to denote that the
|
# The dirty flag is set by *_unless_managed functions to denote that the
|
||||||
# code under transaction management has changed things to require a
|
# code under transaction management has changed things to require a
|
||||||
|
@ -164,6 +165,36 @@ def rollback():
|
||||||
connection._rollback()
|
connection._rollback()
|
||||||
set_clean()
|
set_clean()
|
||||||
|
|
||||||
|
def savepoint():
|
||||||
|
"""
|
||||||
|
Creates a savepoint (if supported and required by the backend) inside the
|
||||||
|
current transaction. Returns an identifier for the savepoint that will be
|
||||||
|
used for the subsequent rollback or commit.
|
||||||
|
"""
|
||||||
|
thread_ident = thread.get_ident()
|
||||||
|
if thread_ident in savepoint_state:
|
||||||
|
savepoint_state[thread_ident].append(None)
|
||||||
|
else:
|
||||||
|
savepoint_state[thread_ident] = [None]
|
||||||
|
tid = str(thread_ident).replace('-', '')
|
||||||
|
sid = "s%s_x%d" % (tid, len(savepoint_state[thread_ident]))
|
||||||
|
connection._savepoint(sid)
|
||||||
|
return sid
|
||||||
|
|
||||||
|
def savepoint_rollback(sid):
|
||||||
|
"""
|
||||||
|
Rolls back the most recent savepoint (if one exists). Does nothing if
|
||||||
|
savepoints are not supported.
|
||||||
|
"""
|
||||||
|
connection._savepoint_rollback(sid)
|
||||||
|
|
||||||
|
def savepoint_commit(sid):
|
||||||
|
"""
|
||||||
|
Commits the most recent savepoint (if one exists). Does nothing if
|
||||||
|
savepoints are not supported.
|
||||||
|
"""
|
||||||
|
connection._savepoint_commit(sid)
|
||||||
|
|
||||||
##############
|
##############
|
||||||
# DECORATORS #
|
# DECORATORS #
|
||||||
##############
|
##############
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
Tests for forcing insert and update queries (instead of Django's normal
|
Tests for forcing insert and update queries (instead of Django's normal
|
||||||
automatic behaviour).
|
automatic behaviour).
|
||||||
"""
|
"""
|
||||||
from django.db import models
|
from django.db import models, transaction
|
||||||
|
|
||||||
class Counter(models.Model):
|
class Counter(models.Model):
|
||||||
name = models.CharField(max_length = 10)
|
name = models.CharField(max_length = 10)
|
||||||
|
@ -40,15 +40,13 @@ ValueError: Cannot force an update in save() with no primary key.
|
||||||
>>> c1.save(force_insert=True)
|
>>> c1.save(force_insert=True)
|
||||||
|
|
||||||
# Won't work because we can't insert a pk of the same value.
|
# Won't work because we can't insert a pk of the same value.
|
||||||
|
>>> sid = transaction.savepoint()
|
||||||
>>> c.value = 5
|
>>> c.value = 5
|
||||||
>>> c.save(force_insert=True)
|
>>> c.save(force_insert=True)
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
IntegrityError: ...
|
IntegrityError: ...
|
||||||
|
>>> transaction.savepoint_rollback(sid)
|
||||||
# Work around transaction failure cleaning up for PostgreSQL.
|
|
||||||
>>> from django.db import connection
|
|
||||||
>>> connection.close()
|
|
||||||
|
|
||||||
# Trying to update should still fail, even with manual primary keys, if the
|
# Trying to update should still fail, even with manual primary keys, if the
|
||||||
# data isn't in the database already.
|
# data isn't in the database already.
|
||||||
|
|
|
@ -6,7 +6,7 @@ To define a one-to-one relationship, use ``OneToOneField()``.
|
||||||
In this example, a ``Place`` optionally can be a ``Restaurant``.
|
In this example, a ``Place`` optionally can be a ``Restaurant``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.db import models, connection
|
from django.db import models, transaction
|
||||||
|
|
||||||
class Place(models.Model):
|
class Place(models.Model):
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
|
@ -178,13 +178,11 @@ DoesNotExist: Restaurant matching query does not exist.
|
||||||
|
|
||||||
# This will fail because each one-to-one field must be unique (and link2=o1 was
|
# This will fail because each one-to-one field must be unique (and link2=o1 was
|
||||||
# used for x1, above).
|
# used for x1, above).
|
||||||
|
>>> sid = transaction.savepoint()
|
||||||
>>> MultiModel(link1=p2, link2=o1, name="x1").save()
|
>>> MultiModel(link1=p2, link2=o1, name="x1").save()
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
IntegrityError: ...
|
IntegrityError: ...
|
||||||
|
>>> transaction.savepoint_rollback(sid)
|
||||||
|
|
||||||
# Because the unittests all use a single connection, we need to force a
|
|
||||||
# reconnect here to ensure the connection is clean (after the previous
|
|
||||||
# IntegrityError).
|
|
||||||
>>> connection.close()
|
|
||||||
"""}
|
"""}
|
||||||
|
|
Loading…
Reference in New Issue