diff --git a/xact.py b/xact.py index c8b5a49..cc59175 100644 --- a/xact.py +++ b/xact.py @@ -1,14 +1,14 @@ """ This code provides a decorator / context manager for transaction management in Django on PostgreSQL. It is intended as a replacement for the existing Django commit_on_success() function, and provides some nice features: - + * Nested transactions: The top-level transaction will be a BEGIN/COMMIT/ROLLBACK block; inner "transactions" are implemented as savepoints. * Commits even if is_dirty is False, eliminating the mistake of forgetting to set the dirty flag when doing database-modifying raw SQL. * Better interaction with pgPool II, if you're using it. * A workaround for a subtle but nasty bug in Django's transaction management. - + As currently implemented, it is NOT thread-safe as a decorator (it IS thread-safe as a context manager). Fix coming. @@ -17,6 +17,8 @@ from functools import wraps +from threading import local + from django.db import transaction, DEFAULT_DB_ALIAS, connections import psycopg2.extensions @@ -24,20 +26,21 @@ class _Transaction(object): def __init__(self, using): self.using = using - self.sid = None - + self._local = local() + def __enter__(self): - if connections[self.using].features.uses_savepoints: + if transaction.is_managed(self.using): # We're already in a transaction; create a savepoint. - self.sid = transaction.savepoint(self.using) + self._local.sid = transaction.savepoint(self.using) else: + self._local.sid = None transaction.enter_transaction_management(using=self.using) transaction.managed(True, using=self.using) - + def __exit__(self, exc_type, exc_value, traceback): if exc_value is None: # commit operation - if self.sid is None: + if self._local.sid is None: # Outer transaction try: transaction.commit(self.using) @@ -49,20 +52,20 @@ def __exit__(self, exc_type, exc_value, traceback): else: # Inner savepoint try: - transaction.savepoint_commit(self.sid, self.using) + transaction.savepoint_commit(self._local.sid, self.using) except: - transaction.savepoint_rollback(self.sid, self.using) + transaction.savepoint_rollback(self._local.sid, self.using) raise else: # rollback operation - if self.sid is None: + if self._local.sid is None: # Outer transaction transaction.rollback(self.using) self._leave_transaction_management() else: # Inner savepoint - transaction.savepoint_rollback(self.sid, self.using) - + transaction.savepoint_rollback(self._local.sid, self.using) + return False def _leave_transaction_management(self): @@ -71,10 +74,10 @@ def _leave_transaction_management(self): connections[self.using]._set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) # Patch for bug in Django's psycopg2 backend; see: # https://code.djangoproject.com/ticket/16047 - + # This is a great recipe for allowing a single object to handle both @ decorators # and with contexts. - + def __call__(self, func): @wraps(func) def inner(*args, **kwargs):