diff --git a/django/bin/daily_cleanup.py b/django/bin/daily_cleanup.py index f77c43357d..6eb5c17feb 100644 --- a/django/bin/daily_cleanup.py +++ b/django/bin/daily_cleanup.py @@ -1,6 +1,6 @@ "Daily cleanup file" -from django.db import backend, connection +from django.db import backend, connection, transaction DOCUMENTATION_DIRECTORY = '/home/html/documentation/' @@ -11,7 +11,7 @@ def clean_up(): (backend.quote_name('core_sessions'), backend.quote_name('expire_date'))) cursor.execute("DELETE FROM %s WHERE %s < NOW() - INTERVAL '1 week'" % \ (backend.quote_name('registration_challenges'), backend.quote_name('request_date'))) - connection.commit() + transaction.commit_unless_managed() if __name__ == "__main__": clean_up() diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 992b429448..26e6afe7ac 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -194,6 +194,10 @@ TIME_FORMAT = 'P' # http://psyco.sourceforge.net/ ENABLE_PSYCO = False +# Do you want to manage transactions manually? +# Hint: you really don't! +TRANSACTIONS_MANAGED = False + ############## # MIDDLEWARE # ############## diff --git a/django/core/cache/backends/db.py b/django/core/cache/backends/db.py index 07ee62273f..1b54addded 100644 --- a/django/core/cache/backends/db.py +++ b/django/core/cache/backends/db.py @@ -1,7 +1,7 @@ "Database cache backend." from django.core.cache.backends.base import BaseCache -from django.db import connection +from django.db import connection, transaction import base64, time from datetime import datetime try: @@ -33,7 +33,7 @@ class CacheClass(BaseCache): now = datetime.now() if row[2] < now: cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % self._table, [key]) - connection.commit() + transaction.commit_unless_managed() return default return pickle.loads(base64.decodestring(row[1])) @@ -58,12 +58,12 @@ class CacheClass(BaseCache): # To be threadsafe, updates/inserts are allowed to fail silently pass else: - connection.commit() + transaction.commit_unless_managed() def delete(self, key): cursor = connection.cursor() cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % self._table, [key]) - connection.commit() + transaction.commit_unless_managed() def has_key(self, key): cursor = connection.cursor() diff --git a/django/core/management.py b/django/core/management.py index cc0b0abde0..7ddf284542 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -218,7 +218,7 @@ get_sql_create.args = APP_ARGS def get_sql_delete(app): "Returns a list of the DROP TABLE SQL statements for the given app." - from django.db import backend, connection, models + from django.db import backend, connection, models, transaction try: cursor = connection.cursor() @@ -233,7 +233,7 @@ def get_sql_delete(app): cursor.execute("SELECT 1 FROM %s LIMIT 1" % backend.quote_name('django_admin_log')) except: # The table doesn't exist, so it doesn't need to be dropped. - connection.rollback() + transaction.rollback_unless_managed() admin_log_exists = False else: admin_log_exists = True @@ -252,7 +252,7 @@ def get_sql_delete(app): cursor.execute("SELECT 1 FROM %s LIMIT 1" % backend.quote_name(klass._meta.db_table)) except: # The table doesn't exist, so it doesn't need to be dropped. - connection.rollback() + transaction.rollback_unless_managed() else: opts = klass._meta for f in opts.fields: @@ -268,7 +268,7 @@ def get_sql_delete(app): cursor.execute("SELECT 1 FROM %s LIMIT 1" % backend.quote_name(klass._meta.db_table)) except: # The table doesn't exist, so it doesn't need to be dropped. - connection.rollback() + transaction.rollback_unless_managed() else: output.append("DROP TABLE %s;" % backend.quote_name(klass._meta.db_table)) if backend.supports_constraints and references_to_delete.has_key(klass): @@ -290,7 +290,7 @@ def get_sql_delete(app): if cursor is not None: cursor.execute("SELECT 1 FROM %s LIMIT 1" % backend.quote_name(f.m2m_db_table())) except: - connection.rollback() + transaction.rollback_unless_managed() else: output.append("DROP TABLE %s;" % backend.quote_name(f.m2m_db_table())) @@ -403,7 +403,7 @@ get_sql_all.args = APP_ARGS # TODO: Check for model validation errors before executing SQL def syncdb(): "Creates the database tables for all apps in INSTALLED_APPS whose tables haven't already been created." - from django.db import backend, connection, models, get_creation_module, get_introspection_module + from django.db import backend, connection, transaction, models, get_creation_module, get_introspection_module introspection_module = get_introspection_module() data_types = get_creation_module().DATA_TYPES @@ -470,7 +470,7 @@ def syncdb(): backend.quote_name(r_col), backend.quote_name(table), backend.quote_name(col)) cursor.execute(sql) - connection.commit() + transaction.commit_unless_managed() syncdb.args = '' def get_admin_index(app): @@ -499,7 +499,7 @@ get_admin_index.args = APP_ARGS def install(app): "Executes the equivalent of 'get_sql_all' in the current database." - from django.db import connection + from django.db import connection, transaction from cStringIO import StringIO app_name = app.__name__[app.__name__.rindex('.')+1:] app_label = app_name.split('.')[-1] @@ -526,15 +526,15 @@ def install(app): Hint: Look at the output of 'django-admin.py sqlall %s'. That's the SQL this command wasn't able to run. The full error: %s\n""" % \ (app_name, app_label, e)) - connection.rollback() + transaction.rollback_unless_managed() sys.exit(1) - connection.commit() + transaction.commit_unless_managed() install.help_doc = "Executes ``sqlall`` for the given app(s) in the current database." install.args = APP_ARGS def reset(app): "Executes the equivalent of 'get_sql_reset' in the current database." - from django.db import connection + from django.db import connection, transaction from cStringIO import StringIO app_name = app.__name__[app.__name__.rindex('.')+1:] app_label = app_name.split('.')[-1] @@ -568,9 +568,9 @@ Type 'yes' to continue, or 'no' to cancel: """) Hint: Look at the output of 'django-admin.py sqlreset %s'. That's the SQL this command wasn't able to run. The full error: %s\n""" % \ (app_name, app_label, e)) - connection.rollback() + transaction.rollback_unless_managed() sys.exit(1) - connection.commit() + transaction.commit_unless_managed() else: print "Reset cancelled." reset.help_doc = "Executes ``sqlreset`` for the given app(s) in the current database." @@ -1035,7 +1035,7 @@ runserver.args = '[optional port number, or ipaddr:port]' def createcachetable(tablename): "Creates the table needed to use the SQL cache backend" - from django.db import backend, get_creation_module, models + from django.db import backend, connection, transaction, get_creation_module, models data_types = get_creation_module().DATA_TYPES fields = ( # "key" is a reserved word in MySQL, so use "cache_key" instead. @@ -1066,7 +1066,7 @@ def createcachetable(tablename): curs.execute("\n".join(full_statement)) for statement in index_output: curs.execute(statement) - connection.commit() + transaction.commit_unless_managed() createcachetable.args = "[tablename]" def run_shell(use_plain=False): diff --git a/django/db/__init__.py b/django/db/__init__.py index 33edda4e5f..54eb4e25c9 100644 --- a/django/db/__init__.py +++ b/django/db/__init__.py @@ -38,4 +38,7 @@ dispatcher.connect(reset_queries, signal=signals.request_started) # Register an event that rolls back the connection # when a Django request has an exception. -dispatcher.connect(lambda: connection.rollback(), signal=signals.got_request_exception) +def _rollback_on_exception(): + from django.db import transaction + transaction.rollback_unless_managed() +dispatcher.connect(_rollback_on_exception, signal=signals.got_request_exception) diff --git a/django/db/backends/ado_mssql/base.py b/django/db/backends/ado_mssql/base.py index b4caebd412..616ad22949 100644 --- a/django/db/backends/ado_mssql/base.py +++ b/django/db/backends/ado_mssql/base.py @@ -64,10 +64,10 @@ class DatabaseWrapper: return base.CursorDebugWrapper(cursor, self) return cursor - def commit(self): + def _commit(self): return self.connection.commit() - def rollback(self): + def _rollback(self): if self.connection: return self.connection.rollback() diff --git a/django/db/backends/dummy/base.py b/django/db/backends/dummy/base.py index 17ef35a4a2..07648033ed 100644 --- a/django/db/backends/dummy/base.py +++ b/django/db/backends/dummy/base.py @@ -17,8 +17,8 @@ class DatabaseError(Exception): class DatabaseWrapper: cursor = complain - commit = complain - rollback = complain + _commit = complain + _rollback = complain def close(self): pass # close() diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 86ea1e5548..4594c07dc0 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -71,10 +71,10 @@ class DatabaseWrapper: return util.CursorDebugWrapper(MysqlDebugWrapper(cursor), self) return cursor - def commit(self): + def _commit(self): self.connection.commit() - def rollback(self): + def _rollback(self): if self.connection: try: self.connection.rollback() diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py index 47fb8e621c..669e8a838a 100644 --- a/django/db/backends/postgresql/base.py +++ b/django/db/backends/postgresql/base.py @@ -37,10 +37,10 @@ class DatabaseWrapper: return util.CursorDebugWrapper(cursor, self) return cursor - def commit(self): + def _commit(self): return self.connection.commit() - def rollback(self): + def _rollback(self): if self.connection: return self.connection.rollback() diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 5b57925256..c50eacc8bf 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -39,10 +39,10 @@ class DatabaseWrapper: else: return cursor - def commit(self): + def _commit(self): self.connection.commit() - def rollback(self): + def _rollback(self): if self.connection: self.connection.rollback() @@ -67,7 +67,6 @@ class SQLiteCursorWrapper(Database.Cursor): return Database.Cursor.executemany(self, query, params) def convert_query(self, query, num_params): - # XXX this seems too simple to be correct... is this right? return query % tuple("?" * num_params) supports_constraints = False diff --git a/django/db/models/base.py b/django/db/models/base.py index 29636c8eb8..7b299c19ac 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -5,7 +5,7 @@ from django.db.models.fields.related import OneToOne, ManyToOne from django.db.models.related import RelatedObject from django.db.models.query import orderlist2sql, delete_objects from django.db.models.options import Options, AdminOptions -from django.db import connection, backend +from django.db import connection, backend, transaction from django.db.models import signals from django.db.models.loading import register_models from django.dispatch import dispatcher @@ -184,7 +184,7 @@ class Model(object): ','.join(placeholders)), db_values) if self._meta.has_auto_field and not pk_set: setattr(self, self._meta.pk.attname, backend.get_last_insert_id(cursor, self._meta.db_table, self._meta.pk.column)) - connection.commit() + transaction.commit_unless_managed() # Run any post-save hooks. dispatcher.send(signal=signals.post_save, sender=self.__class__, instance=self) @@ -340,7 +340,7 @@ class Model(object): backend.quote_name(rel_field.m2m_column_name()), backend.quote_name(rel_field.m2m_reverse_name())) cursor.executemany(sql, [(this_id, i) for i in id_list]) - connection.commit() + transaction.commit_unless_managed() ############################################ # HELPER FUNCTIONS (CURRIED MODEL METHODS) # @@ -357,7 +357,7 @@ def method_set_order(ordered_obj, self, id_list): backend.quote_name(ordered_obj.pk.column)) rel_val = getattr(self, ordered_obj.order_with_respect_to.rel.field_name) cursor.executemany(sql, [(i, rel_val, j) for i, j in enumerate(id_list)]) - connection.commit() + transaction.commit_unless_managed() def method_get_order(ordered_obj, self): cursor = connection.cursor() diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 9e4c976b4d..f1dce24e43 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -1,4 +1,4 @@ -from django.db import backend, connection +from django.db import backend, connection, transaction from django.db.models import signals from django.db.models.fields import AutoField, Field, IntegerField from django.db.models.related import RelatedObject @@ -231,7 +231,7 @@ def _add_m2m_items(rel_manager_inst, managerclass, rel_model, join_table, source cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ (join_table, source_col_name, target_col_name), [source_pk_val, obj_id]) - connection.commit() + transaction.commit_unless_managed() def _remove_m2m_items(rel_model, join_table, source_col_name, target_col_name, source_pk_val, *objs): @@ -255,7 +255,7 @@ def _remove_m2m_items(rel_model, join_table, source_col_name, cursor.execute("DELETE FROM %s WHERE %s = %%s AND %s = %%s" % \ (join_table, source_col_name, target_col_name), [source_pk_val, obj._get_pk_val()]) - connection.commit() + transaction.commit_unless_managed() def _clear_m2m_items(join_table, source_col_name, source_pk_val): # Utility function used by the ManyRelatedObjectsDescriptors @@ -268,7 +268,7 @@ def _clear_m2m_items(join_table, source_col_name, source_pk_val): cursor.execute("DELETE FROM %s WHERE %s = %%s" % \ (join_table, source_col_name), [source_pk_val]) - connection.commit() + transaction.commit_unless_managed() class ManyRelatedObjectsDescriptor(object): # This class provides the functionality that makes the related-object diff --git a/django/db/models/query.py b/django/db/models/query.py index cfd86892fd..e5cfd2c75d 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -1,4 +1,4 @@ -from django.db import backend, connection +from django.db import backend, connection, transaction from django.db.models.fields import DateField, FieldDoesNotExist from django.db.models import signals from django.dispatch import dispatcher @@ -846,4 +846,4 @@ def delete_objects(seen_objs): setattr(instance, cls._meta.pk.attname, None) dispatcher.send(signal=signals.post_delete, sender=cls, instance=instance) - connection.commit() + transaction.commit_unless_managed() diff --git a/django/db/transaction.py b/django/db/transaction.py new file mode 100644 index 0000000000..9af5b4e5ab --- /dev/null +++ b/django/db/transaction.py @@ -0,0 +1,225 @@ +""" +This module implements a transaction manager that can be used to define +transaction handling in a request or view function. It is used by transaction +control middleware and decorators. + +The transaction manager can be in managed or in auto state. Auto state means the +system is using a commit-on-save strategy (actually it's more like +commit-on-change). As soon as the .save() or .delete() (or related) methods are +called, a commit is made. + +Managed transactions don't do those commits, but will need some kind of manual +or implicit commits or rollbacks. +""" + +import thread +from django.db import connection +from django.conf import settings + +class TransactionManagementError(Exception): + """ + This is the exception that is thrown when + something bad happens with transaction management. + """ + pass + +# The state is a dictionary of lists. The key to the dict is the current +# thread and the list is handled as a stack of values. +state = {} + +# The dirty flag is set by *_unless_managed functions to denote that the +# code under transaction management has changed things to require a +# database commit. +dirty = {} + +def enter_transaction_management(): + """ + Enters transaction management for a running thread. It must be balanced with + the appropriate leave_transaction_management call, since the actual state is + managed as a stack. + + The state and dirty flag are carried over from the surrounding block or + from the settings, if there is no surrounding block (dirty is allways false + when no current block is running). + """ + thread_ident = thread.get_ident() + if state.has_key(thread_ident) and state[thread_ident]: + state[thread_ident].append(state[thread_ident][-1]) + else: + state[thread_ident] = [] + state[thread_ident].append(settings.TRANSACTIONS_MANAGED) + if not dirty.has_key(thread_ident): + dirty[thread_ident] = False + +def leave_transaction_management(): + """ + Leaves transaction management for a running thread. A dirty flag is carried + over to the surrounding block, as a commit will commit all changes, even + those from outside (commits are on connection level). + """ + thread_ident = thread.get_ident() + if state.has_key(thread_ident) and state[thread_ident]: + del state[thread_ident][-1] + else: + raise TransactionManagementError("This code isn't under transaction management") + if dirty.get(thread_ident, False): + # I fixed it for you this time, but don't do it again! + rollback() + raise TransactionManagementError("Transaction managed block ended with pending COMMIT/ROLLBACK") + dirty[thread_ident] = False + +def is_dirty(): + """ + Checks if the current transaction requires a commit for changes to happen. + """ + return dirty.get(thread.get_ident(), False) + +def set_dirty(): + """ + Sets a dirty flag for the current thread and code streak. This can be used + to decide in a managed block of code to decide whether there are open + changes waiting for commit. + """ + thread_ident = thread.get_ident() + if dirty.has_key(thread_ident): + dirty[thread_ident] = True + else: + raise TransactionManagementError("This code isn't under transaction management") + +def set_clean(): + """ + Resets a dirty flag for the current thread and code streak. This can be used + to decide in a managed block of code to decide whether there should happen a + commit or rollback. + """ + thread_ident = thread.get_ident() + if dirty.has_key(thread_ident): + dirty[thread_ident] = False + else: + raise TransactionManagementError("This code isn't under transaction management") + +def is_managed(): + """ + Checks whether the transaction manager is in manual or in auto state. + """ + thread_ident = thread.get_ident() + if state.has_key(thread_ident): + if state[thread_ident]: + return state[thread_ident][-1] + return settings.TRANSACTIONS_MANAGED + +def managed(flag=True): + """ + Puts the transaction manager into a manual state - managed transactions have + to be committed explicitely by the user. If you switch off transaction + management and there is a pending commit/rollback, the data will be + commited. + """ + thread_ident = thread.get_ident() + top = state.get(thread_ident, None) + if top: + top[-1] = flag + if not flag and is_dirty(): + connection._commit() + set_clean() + else: + raise TransactionManagementError("This code isn't under transaction management") + +def commit_unless_managed(): + """ + Commits changes if the system is not in managed transaction mode. + """ + if not is_managed(): + connection._commit() + else: + set_dirty() + +def rollback_unless_managed(): + """ + Rolls back changes if the system is not in managed transaction mode. + """ + if not is_managed(): + connection._rollback() + else: + set_dirty() + +def commit(): + """ + Does the commit itself and resets the dirty flag. + """ + connection._commit() + set_clean() + +def rollback(): + """ + This function does the rollback itself and resets the dirty flag. + """ + connection._rollback() + set_clean() + +############## +# DECORATORS # +############## + +def autocommit(func): + """ + Decorator that activates commit on save. This is Django's default behavour; + this decorator is useful if you globally activated transaction management in + your settings file and want the default behaviour in some view functions. + """ + + def _autocommit(*args, **kw): + try: + enter_transaction_management() + managed(False) + return func(*args, **kw) + finally: + leave_transaction_management() + + return _autocommit + +def commit_on_success(func): + """ + This decorator activates commit on response. This way if the viewfunction + runs successfully, a commit is made, if the viewfunc produces an exception, + a rollback is made. This is one of the most common ways to do transaction + control in web apps. + """ + + def _commit_on_success(*args, **kw): + try: + enter_transaction_management() + managed(True) + try: + res = func(*args, **kw) + except Exception, e: + if is_dirty(): + rollback() + raise e + else: + if is_dirty(): + commit() + return res + finally: + leave_transaction_management() + + return _commit_on_success + +def commit_manually(func): + """ + Decorator that activates manual transaction control. It just disables + automatic transaction control and doesn't do any commit/rollback of it's own + - it's up to the user to call the commit and rollback functions themselves. + """ + + def _commit_manually(*args, **kw): + try: + enter_transaction_management() + managed(True) + return func(*args, **kw) + finally: + leave_transaction_management() + + return _commit_manually + + diff --git a/django/middleware/transaction.py b/django/middleware/transaction.py new file mode 100644 index 0000000000..aa35cc1d2d --- /dev/null +++ b/django/middleware/transaction.py @@ -0,0 +1,29 @@ +from django.conf import settings +from django.db import transaction + +class TransactionMiddleware: + """ + Transaction middleware. If this is enabled, each view function will be run + with commit_on_response activated - that way a save() doesn't do a direct + commit, the commit is done when a successfull response is created. If an + exception happens, the database is rolled back. + """ + + def process_request(self, request): + """Enters transaction management""" + transaction.enter_transaction_management() + transaction.managed(True) + + def process_exception(self, request, exception): + """Rolls back the database and leaves transaction management""" + if transaction.is_dirty(): + transaction.rollback() + transaction.leave_transaction_management() + + def process_response(self, request, response): + """Commits and leaves transaction management.""" + if transaction.is_managed(): + if transaction.is_dirty(): + transaction.commit() + transaction.leave_transaction_management() + return response \ No newline at end of file diff --git a/docs/middleware.txt b/docs/middleware.txt index d46960a1d2..1631af471d 100644 --- a/docs/middleware.txt +++ b/docs/middleware.txt @@ -102,6 +102,23 @@ Enables session support. See the `session documentation`_. .. _`session documentation`: http://www.djangoproject.com/documentation/sessions/ +django.middleware.transaction.TransactionMiddleware +--------------------------------------------------- + +Binds commit and rollback to the request/response phase. If a view function runs +successfully, a commit is done. If it fails with an exception, a rollback is +done. + +The order of this middleware in the stack is important: middleware modules +running outside of it run with commit-on-save - the default Django behavior. +Middleware modules running inside it (coming later in the stack) will be under +the same transaction control as the view functions. + +See the `transaction management documentation`_. + +.. _`transaction management documentation`: http://www.djangoproject.com/documentation/transaction/ + + Writing your own middleware =========================== diff --git a/docs/transactions.txt b/docs/transactions.txt new file mode 100644 index 0000000000..3b738f23b3 --- /dev/null +++ b/docs/transactions.txt @@ -0,0 +1,138 @@ +============================== +Managing database transactions +============================== + +Django gives you a few ways to control how database transactions are managed. + +Django's default transaction behavior +===================================== + +The default behavior of Django is to commit on special model functions. If you +call ``model.save()`` or ``model.delete()``, that change will be committed immediately. + +This is much like the auto-commit setting for most databases: as soon as you +perform an action that needs to write to the database, Django produces the +insert/update/delete statements and then does the commit. There is no implicit +rollback in Django. + +Tying transactions to HTTP requests +=================================== + +A useful way to handle transactions is to tie them to the request and response +phases. + +When a request starts, you start a transaction. If the response is produced +without problems, any transactions are committed. If the view function produces +and exception, a rollback happens. This is one of the more intuitive ways to +handle transactions. To activate this feature, just add the +``TransactionMiddleware`` middleware to your stack:: + + MIDDLEWARE_CLASSES = ( + "django.middleware.common.CommonMiddleware", + "django.middleware.sessions.SessionMiddleware", + "django.middleware.cache.CacheMiddleware", + "django.middleware.transaction.TransactionMiddleware", + ) + +The order is quite important: the transaction middleware will be relevant not +only for the view functions called, but for all middleware modules that come +after it. So if you use the session middleware after the transaction middleware, +session creation will be part of the transaction. + +The cache middleware isn't affected as it uses it's own database cursor (that is +mapped to it's own database connection internally) and only the database based +cache is affected. + +Controlling transaction management in views +=========================================== + +For many people, implicit request-based transactions will work wonderfully. +However, if you need to control the way that transactions are managed, +there are a set of decorators that you can apply to a function to change +the way transactions are handled. + +.. note:: + + Although the examples below use view functions as examples, these + decorators can be applied to non-view functions as well. + +``autocommit`` +-------------- + +You can use the ``autocommit`` decorator to switch a view function to the +default commit behavior of Django, regardless of the global setting. Just use +the decorator like this:: + + from django.db.transaction import autocommit + + @transaction.autocommit + def viewfunc(request): + .... + +Within ``viewfunc`` transactions will be comitted as soon as you call +``model.save()``, ``model.delete()``, or any similar function that writes to the +database. + +``commit_on_success`` +--------------------- + +You can use the ``commit_on_success`` decorator to use a single transaction for +all the work done in a function:: + + from django.db.transaction import commit_on_success + + @commit_on_success + def viewfunc(request): + .... + +If the function returns successfully then all work done will be committed. If an +exception is raised beyond the function, however, the transaction will be rolled +back. + +``commit_manually`` +------------------- + +Sometimes you need full control over your transactions. In that case, you can use the +``commit_manually`` decorator which will make you run your own transaction management. + +If you don't commit or rollback and did change data (so that the current transaction +is marked as dirty), you will get a ``TransactionManagementError`` exception saying so. + +Manual transaction management looks like:: + + from django.db import transaction + + @transaction.commit_manually + def viewfunc(request): + ... + transaction.commit() + ... + try: + ... + except: + transaction.rollback() + else: + transaction.commit() + +..admonition:: An important note to users of earlier django releases: + + The database ``connection.commit`` and ``connection.rollback`` functions + (also called ``db.commit`` and ``db.rollback`` in 0.91 and earlier), no + longer exist and have been replaced by the ``transaction.commit`` and + ``transaction.rollback`` commands. + +How to globally deactivate transaction management +================================================= + +Control freaks can totally disable all transaction management by setting +``DISABLE_TRANSACTION_MANAGEMENT`` to ``True`` in your settings file. + +If you do this, there will be no management whatsoever. The middleware will no +longer implicitly commit transactions, and you'll need to roll management +yourself. This even will require you to commit changes done by middleware +somewhere else. + +Thus, this is best used in situations where you want to run your own transaction +controlling middleware or do something really strange. In almost all situations +you'll be better off using the default behavior or the transaction middleware +and only modify selected functions as needed. \ No newline at end of file diff --git a/tests/modeltests/transactions/__init__.py b/tests/modeltests/transactions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/modeltests/transactions/models.py b/tests/modeltests/transactions/models.py new file mode 100644 index 0000000000..6c5d1de0a5 --- /dev/null +++ b/tests/modeltests/transactions/models.py @@ -0,0 +1,91 @@ +""" +XXX. Transactions + +Django handles transactions in three different ways. The default is to commit +each transaction upon a write, but you can decorate a function to get +commit-on-sucess behavior, or else you can manage the transaction manually. +""" + +from django.db import models + +class Reporter(models.Model): + first_name = models.CharField(maxlength=30) + last_name = models.CharField(maxlength=30) + email = models.EmailField() + + def __repr__(self): + return "%s %s" % (self.first_name, self.last_name) + +API_TESTS = """ +>>> from django.db import connection, transaction + +# the default behavior is to autocommit after each save() action +>>> def create_a_reporter_then_fail(first, last): +... a = Reporter(first_name=first, last_name=last) +... a.save() +... raise Exception("I meant to do that") +... +>>> create_a_reporter_then_fail("Alice", "Smith") +Traceback (most recent call last): + ... +Exception: I meant to do that + +# The object created before the exception still exists +>>> Reporter.objects.all() +[Alice Smith] + +# the autocommit decorator works exactly the same as the default behavior +>>> autocomitted_create_then_fail = transaction.autocommit(create_a_reporter_then_fail) +>>> autocomitted_create_then_fail("Ben", "Jones") +Traceback (most recent call last): + ... +Exception: I meant to do that + +# Same behavior as before +>>> Reporter.objects.all() +[Alice Smith, Ben Jones] + +# With the commit_on_success decorator, the transaction is only comitted if the +# function doesn't throw an exception +>>> committed_on_success = transaction.commit_on_success(create_a_reporter_then_fail) +>>> committed_on_success("Carol", "Doe") +Traceback (most recent call last): + ... +Exception: I meant to do that + +# This time the object never got saved +>>> Reporter.objects.all() +[Alice Smith, Ben Jones] + +# If there aren't any exceptions, the data will get saved +>>> def remove_a_reporter(): +... r = Reporter.objects.get(first_name="Alice") +... r.delete() +... +>>> remove_comitted_on_success = transaction.commit_on_success(remove_a_reporter) +>>> remove_comitted_on_success() +>>> Reporter.objects.all() +[Ben Jones] + +# You can manually manage transactions if you really want to, but you +# have to remember to commit/rollback +>>> def manually_managed(): +... r = Reporter(first_name="Carol", last_name="Doe") +... r.save() +... transaction.commit() +>>> manually_managed = transaction.commit_manually(manually_managed) +>>> manually_managed() +>>> Reporter.objects.all() +[Ben Jones, Carol Doe] + +# If you forget, you'll get bad errors +>>> def manually_managed_mistake(): +... r = Reporter(first_name="David", last_name="Davidson") +... r.save() +... # oops, I forgot to commit/rollback! +>>> manually_managed_mistake = transaction.commit_manually(manually_managed_mistake) +>>> manually_managed_mistake() +Traceback (most recent call last): + ... +TransactionManagementError: Transaction managed block ended with pending COMMIT/ROLLBACK +""" \ No newline at end of file diff --git a/tests/runtests.py b/tests/runtests.py index d783fe151b..3ea256869d 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -39,13 +39,13 @@ class DjangoDoctestRunner(doctest.DocTestRunner): "Code: %r\nLine: %s\nExpected: %r\nGot: %r" % (example.source.strip(), example.lineno, example.want, got)) def report_unexpected_exception(self, out, test, example, exc_info): - from django.db import connection + from django.db import transaction tb = ''.join(traceback.format_exception(*exc_info)[1:]) log_error(test.name, "API test raised an exception", "Code: %r\nLine: %s\nException: %s" % (example.source.strip(), example.lineno, tb)) # Rollback, in case of database errors. Otherwise they'd have # side effects on other tests. - connection.rollback() + transaction.rollback_unless_managed() normalize_long_ints = lambda s: re.sub(r'(?