Database Triggers Aren't Evil, and They Actually Kind of Rock
Who says they suck?
Nobody that I’ve seen has come out and actually said that they don’t like
database triggers, but at the same time, Python (Django) programmers like to
program in Python. And PL/pgSQL certainly is not Python. There’s a tendency to
do everything in Python–especially with the use of Django’s dispatcher.
But there’s some serious overhead with that approach, and roundtrips, and race
conditions, etc. If you’re using a good database, there’s an alternative:
you guessed it, database triggers.
Let’s set up some models
Here will be our models for the remainder of this post:
fromdjango.db.modelsimportsignalsfrombookmarksimportmodelssql="""CREATE LANGUAGE plpgsql;CREATE OR REPLACE FUNCTION update_bookmark_aggregate_trigger() RETURNS "trigger" AS ' DECLARE new_score INTEGER; new_num_votes INTEGER; BEGIN SELECT COUNT(*) INTO STRICT new_num_votes FROM bookmarks_vote WHERE bookmark_id = NEW.bookmark_id; SELECT COALESCE(SUM(value), 0) INTO STRICT new_score FROM bookmarks_vote WHERE bookmark_id = NEW.bookmark_id; UPDATE bookmarks_bookmark SET score = new_score, num_votes = new_num_votes WHERE id = NEW.bookmark_id; RETURN NEW; END;' LANGUAGE 'plpgsql' VOLATILE;CREATE TRIGGER update_bookmark_aggregate_trigger AFTER INSERT OR UPDATE ON bookmarks_vote FOR EACH ROW EXECUTE PROCEDURE update_bookmark_aggregate_trigger();"""defcreate_trigger(app,created_models,verbosity,**kwargs):fromdjango.dbimportconnectioncursor=connection.cursor()cursor.execute(sql)signals.post_syncdb.connect(create_trigger,sender=models)
In this file we have declared two variables, new_score, and
new_num_votes. We do two queries to get the aggregate data. And then we
update the bookmark to reflect the new aggregated values. This script is
executed once when the bookmarks models are first loaded into the database, and
we’re all set!
Let’s see how it works
1234567891011121314151617
>>> frombookmarks.modelsimportBookmark,Vote>>> b=Bookmark.objects.create(title="Blog",url='http://eflorenzano.com/')>>> b.num_votes0>>> b.score0# There is no aggregate data yet>>> Vote.objects.create(bookmark=b,value=1)<Vote: Vote object>>>> Vote.objects.create(bookmark=b,value=2)<Vote: Vote object># We need to re-query for the bookmark, due to no identity map in Django.>>> b=Bookmark.objects.all()[0]>>> b.num_votes2>>> b.score3
Voila! This was all done in the database behind the scenes. Very cool, very
fast, and it kind of rocks.