Welcome to django-transaction-hooks!¶
A better alternative to the transaction signals Django will never have.
Sometimes you need to fire off an action related to the current database transaction, but only if the transaction successfully commits. Examples: a Celery task, an email notification, or a cache invalidation.
Doing this correctly while accounting for savepoints that might be individually rolled back, closed/dropped connections, and idiosyncrasies of various databases, is non-trivial. Transaction signals just make it easier to do it wrong.
django-transaction-hooks
does the heavy lifting so you don’t have to.
Prerequisites¶
django-transaction-hooks
supports Django 1.6.x through 1.8.x on Python
2.6, 2.7, 3.2, 3.3 and 3.4.
django-transaction-hooks
has been merged into Django 1.9 and is now a
built-in feature, so this third-party library should not be used with Django
1.9+.
SQLite3, PostgreSQL (+ PostGIS), and MySQL are currently the only databases with built-in support; you can experiment with whether it works for your favorite database backend with just a few lines of code.
Installation¶
django-transaction-hooks
is available on PyPI. Install it with:
pip install django-transaction-hooks
Setup¶
django-transaction-hooks
is implemented via custom database backends. (Why
backends?)
For example, to use the PosgreSQL backend, set the ENGINE
in your
DATABASES
setting to transaction_hooks.backends.postgresql_psycopg2
(in
place of django.db.backends.postgresql_psycopg2
). For example:
DATABASES = {
'default': {
'ENGINE': 'transaction_hooks.backends.postgresql_psycopg2',
'NAME': 'foo',
},
}
MySQL, SQLite, and PostGIS are similarly supported, via
transaction_hooks.backends.mysql
, transaction_hooks.backends.sqlite3
,
and transaction_hooks.backends.postgis
.
Using the mixin¶
If you’re currently using Django’s built-in database backend for SQLite,
Postgres, PostGIS, or MySQL, you can skip this section; just use the
appropriate backend from transaction_hooks.backends
as outlined above.
Not using one of those? No worries - all the magic happens in a mixin, so making it happen with your favorite database backend may not be hard (no guarantees it’ll work right, though.)
You’ll need to create your own custom backend that inherits both from
transaction_hooks.mixin.TransactionHooksDatabaseWrapperMixin
and from the
database backend you’re currently using. To do this, make a Python package (a
directory with an __init__.py
file in it) somewhere, and then put a
base.py
module inside that package. Its contents should look something like
this:
from django.db.backends.postgresql_psycopg2 import base
from transaction_hooks.mixin import TransactionHooksDatabaseWrapperMixin
class DatabaseWrapper(TransactionHooksDatabaseWrapperMixin,
base.DatabaseWrapper):
pass
Obviously you’ll want to replace django.db.backends.postgresql_psycopg2
with whatever existing backend you are currently using.
Then set your database ENGINE
(as above) to the Python dotted path to the
package containing that base.py
module. For example, if you put the above
code in myproject/mybackend/base.py
, your ENGINE
setting would be
myproject.mybackend
.
Usage¶
Pass any function (that takes no arguments) to connection.on_commit
:
from django.db import connection
def do_something():
# send a mail, fire off a Celery task, what-have-you.
connection.on_commit(do_something)
You can also wrap your thing up in a lambda:
connection.on_commit(lambda: some_celery_task.delay('arg1'))
The function you pass in will be called immediately after a hypothetical database write made at the same point in your code is successfully committed. If that hypothetical database write is instead rolled back, your function will be discarded and never called.
If you register a callback while there is no transaction active, it will be executed immediately.
Notes¶
Warning
This code is new, not yet battle-tested, and probably has bugs. If you find one, please report it.
Use autocommit and transaction.atomic¶
django-transaction-hooks
is only built and tested to work correctly in
autocommit mode, which is the default in Django 1.6+, and with the
transaction.atomic / ATOMIC_REQUESTS transaction API. If you set
autocommit off on your connection and/or use lower-level transaction APIs
directly, django-transaction-hooks
likely won’t work as you expect.
For instance, commit hooks are not run until autocommit is restored on the
connection following the commit (because otherwise any queries done in a commit
hook would open an implicit transaction, preventing the connection from going
back into autocommit mode). Also, even though with autocommit off you’d
generally be in an implicit transaction outside of any atomic
block,
callback hooks registered outside an atomic
block will still run
immediately, not on commit. And there are probably more gotchas here.
Use autocommit mode and transaction.atomic (or ATOMIC_REQUESTS) and you’ll be happier.
Order of execution¶
On-commit hooks for a given transaction are executed in the order they were registered.
Exception handling¶
If one on-commit hook within a given transaction raises an uncaught
exception, no later-registered hooks in that same transaction will run. (This
is, of course, the same behavior as if you’d executed the hooks sequentially
yourself without on_commit()
.)
Timing of execution¶
Your hook functions are executed after a successful commit, so if they fail,
it will not cause the transaction to roll back. They are executed conditionally
upon the success of the transaction, but they are not part of the
transaction. For the intended use cases (mail notifications, Celery tasks,
etc), this is probably fine. If it’s not (if your follow-up action is so
critical that its failure should mean the failure of the transaction itself),
then you don’t want django-transaction-hooks
. (Instead, you may want
two-phase commit.)
Use with South¶
If you use South, you will probably need to set the
SOUTH_DATABASE_ADAPTERS setting when you switch to a custom database backend
(e.g. to {'default': 'south.db.postgresql_psycopg2'}
, if you are using
PostgreSQL).
Use in tests¶
Django’s TestCase class wraps each test in a transaction and rolls back that
transaction after each test, in order to provide test isolation. This means
that no transaction is ever actually committed, thus your on_commit
hooks
will never be run. If you need to test the results of an on_commit
hook,
you may need to use TransactionTestCase instead.
Savepoints¶
Savepoints (i.e. nested transaction.atomic
blocks) are handled
correctly. That is, an on_commit
hook registered after a savepoint (in a
nested atomic
block) will be called after the outer transaction is
committed, but not if a rollback to that savepoint or any previous savepoint
occurred during the transaction.
Why database backends?¶
Yeah, it’s a bit of a pain. But since all transaction state is stored on the database connection object, this is the only way it can be done without monkeypatching. And I hate monkeypatching.
(The worst bit about a custom database backend is that if you need two different ones, they can be hard or impossible to compose together. In this case, the mixin should make that less painful.)
If this turns out to be really popular, it might be possible to get something like it into the Django core backends, which would remove that issue entirely.
Why no rollback hook?¶
A rollback hook is even harder to implement robustly than a commit hook, since a variety of things can cause an implicit rollback. For instance, your database connection was dropped because your process was killed without a chance to shutdown gracefully: your rollback hook will never run.
The solution is simple: instead of doing something during the atomic block
(transaction) and then undoing it if the transaction fails, use on_commit
to delay doing it in the first place until after the transaction succeeds. It’s
a lot easier to undo something you never did in the first place!
Contributing¶
See the contributing docs.