The Bad Parts of Django
I’ve recently come into contact with a fairly large django codebase. Django holds a special place in my heart, since it was the first web framework I ever used. At the time it seemed great; it allowed me, someone who didn’t know what an “ajax request” was, to get pretty far pretty quickly.
Now that I’ve been doing this stuff professionally for a few years, I have opinions, even though I’m probably still not qualified to.
What I’ve come to feel is that django encourages ease over simplicity, in the sense that Rich Hickey defines the terms in his classic “Simple Made Easy” talk. This is fun and productive at first, but without intervention, it’s easy to build yourself a bit of a mess.
I’m going to start with my biggest gripe, partly because this section has the highest potential of saving someone else from a headache down the road. The django orm uses autocommit by default. This basically means that requests are processed outside the context of a wrapping transaction, so any intermediate database state mutation is commited whenever the queries are sent to the database, which might happen long before the request completes.
I haven’t been around long enough to say how common this is, but I will say that in my experience, tying the request lifecycle to a transaction is a wonderful thing. Not having it leads to:
- poor data consistency guarantees. request dies on a transient exception half-way through a complex multi-table operation? oh well.
- weird business-logic-level workarounds, like doing lots of pre-flight checks to minimize the chances of uncaught exceptions after mutations have been committed.
- isolation issues. transactions don’t necessarily “solve” this as behavior still depends on your isolation level, but at least it gives you the choice.
Now, django does provide support for transactions via the
atomic context manager. It even provides the
ATOMIC_REQUESTS setting, which does exactly what I’m complaining it lacks. The issue is that it defaults to
Here’s what the docs on this setting say:
While the simplicity of this transaction model is appealing, it also makes it inefficient when traffic increases. Opening a transaction for every view has some overhead. The impact on performance depends on the query patterns of your application and on how well your database handles locking.
This is a case of optimizing for the wrong thing. It might not scale well eventually? Sure, that’s true of everything. Much better to wrap requests with transactions by default and optimize from there, than just throw the whole model away. The result is that by default, django essentially treats your database like shared memory between your application workers, instead of the wonderfully powerful abstraction that it is. And how many people reaching for django for the first time are going to tune database configuration defaults?
It’s perhaps vapid to point out, but the django orm’s
get_or_create function is emblematic of the endenmic priotization of “easiness” that runs throughout django.
As I write, I’m actually trying very hard to think of situations that would justify the use of such a function; I’m sure they exist. However, in those cases, one should suck it up and write a ten line function that gets or creates that special entity where
get_or_create_unicorn("Bob") is actually something you want to see when reading the code.
This one is likely to be controversal, but if I’m doing this right then everything I’ve already said is too.
I should start out by saying that the django shell is actually cool and good and is somewhat analagous to what clojurists would call “repl-driven development.”
But, (but,) it seems to encourage what I’ll call the “third wheel” model of software engineering. In a healthy system, the software and the business are in a happy, mostly-loving relationship, only calling upon the engineer when one end isn’t holding up its end of the bargain (or more often, when the bargain needs changing.) There might be some long nights of shoulder crying and ice cream consuming, but by and large the business and the software get along great, with little ongoing operational input needed by the engineer.
In the third wheel model, the engineer is a constant source of support and assistance in the relationship. This might sound simply like the natural way of things; what else is the engineer for? The difference is that in a healthy relationship, the engineer is only expected to assist with novel problems, not to mediate a dirty dishes dispute for the tenth time. The engineer does his job by looking at the novel problem and modifying the software (and sometimes the business) so that he doesn’t need to get involved anymore.
In the third wheel model, the engineer responds to the tenth dirty dishes dispute by firing up his django shell and running
clean_dirty_dishes(), because it’s easier than actually developing, testing, and shipping a proper fix.
Now you might say I’m being an idealist here, and you’re right, I am. I recognize that at a certain scale, the third wheel model is actually a reasonable model, because in the extreme case you might be the business. Or maybe the business is you and a couple others, but you get my point. The thing is that as you grow, you have to relinquish being the third wheel and remove yourself from the day-to-day functioning of the relationship. Otherwise you end up with the fix for common issues being “have an engineer run this script in his django shell”, instead of giving the rest of the business the tools that it needs to avoid or solve the problem itself.
I realize this isn’t really the direct fault of the django shell, and is more of a managerial issue. But this is a personal blog, so forgive me if I sometimes wander.
Django is still pretty good. You should probably enable
ATOMIC_REQUESTS unless you have a good reason not to.
get_or_create() offends my delicate aesthetic senses. The django shell is good up until the point at which it becomes a part of your operational model.