Django ORM

Django has tight integration between relational databases, HTML, and REST APIs. By defining one model, you get a web interface to modify it, a database interface to query or modify it, and HTML interfaces (forms) to query/modify it. This is fantastic for rapid development.

So, if you're in that tightly-coupled ecosystem and legitimately making use of it, then bear those pros in mind.

I want to talk about when you're not using Django's Admin interface, and HTML forms and templates. This is much more likely for any large application.

Related Article: https://calpaterson.com/activerecord.html

The pain

Django's ORM follows

I want to talk about Django's ORM and my explain my thoughts on why I think it should be avoided.

It's not SQL

Django goes out of its way to hide SQL, database principles and Python's standard interface to databases. This means less transferable knowledge is learnt, and actual understanding about databases is harder to come by.

SQLAlchemy maps much more directly onto

Joins

Let's do a simple join:

Books.objects.filter(author__name="django")

It's not obvious at all that a join is happening.

Aggregates

Book.objects.annotate(Count('chapters'))

vs

SELECT book.*, count(chapter.id)
FROM book
JOIN chapter using (book_id);

It's not Python's DBAPI

...

It hides the global state

From anywhere you can call the database:

Book.objects.create()

That is actually pretty bad, but further this demonstrates that the database connection, cursor, session, transaction are all hidden away.

with Session(engine) as session:
    session.add(book)
    session.commit()

Further reading: https://docs.sqlalchemy.org/en/14/orm/session_transaction.html?highlight=savepoint#session-level-vs-engine-level-transaction-control

It uses autocommit

By default, Django uses autocommit where every statement is immediately committed. This is against the PEP 249 standard and usually not what you want.

Complex applications often perform complex queries and mutations that must be kept consistent, and thus

To use a transaction:

with transaction.atomic(durable=True):
    obj1.save()
    obj2.save()

SQLAlchemy ORM:

with session.begin():
    session.add(obj1)
    session.add(obj2)

Further reading: https://docs.sqlalchemy.org/en/14/changelog/migration_20.html#library-level-but-not-driver-level-autocommit-removed-from-both-core-and-orm

It mixes together too many things

  • Model fields can define blank which is a client-side validation.
  • Model fields can define verbose_name which is for displaying to users.

It's not clear when queries are executed

There are a lot of rules on when a query is actually executed. I'll grant they're pretty straightforward, but there are edge cases to ensure multiple queries are not executed by accident.

results = Book.objects.all()
if len(results) > 2:
    print(results)

In comparison, with SQLAlchemy there is an explicit execute:

results = session.execute(select(Book))

Complex queries are nasty

SQLAlchemy has strong support for niche features in comparison to Django.

Transactions are a mess

Though in Django 3.2, they finally added transaction(durable=True), the whole transaction system is opaque.

It's not DDD-compatible

Django forces you to define your models in the root of an "app", while with a good code structure the database models will be hidden away.