O is for ORM - Python A to Z
Code in this blog post was written with versions: Python: 3.14, django: 6.1
Python A-Z is a blog series about Python. Each day, I share insights, ideas and examples for different parts of Python development that match with the letter of the day. Blaugust is an annual blogging festival in August where the goal is to write a blog post every day of the month.
Last year when I gave a talk Love Letter to Django in a local meetup, my main thread was about its ORM (Object–relational mapping) and a few things it enables.
I’m a big fan of ORM as a model: abstracting the database layer behind models that model the business logic of your application rather than writing raw SQL at the end of every function call path makes writing program logic way more enjoyable.
Here’s a small excerpt from one of my apps.
from django.db import models
from django.conf import settings
from api.models import Card
class Collection(models.Model):
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
name = models.CharField(blank=True, null=True)
def __str__(self):
if self.name:
return f"Collection(name={self.name})"
else:
return f"Collection(name={self.owner.username}'s collection)"
class CollectionItem(models.Model):
collection = models.ForeignKey(Collection, on_delete=models.CASCADE)
variant = models.ForeignKey(CollectionVariant, on_delete=models.CASCADE)
quantity = models.IntegerField(default=0)
def __str__(self):
return f"{self.variant.card}, {self.variant.variant}, x{self.quantity}"
I can then query my collection items in different ways.
## In a view based on the logged in user
def add_to_collection(request, variant_id: str) -> HttpResponse:
collection = Collection.objects.filter(owner=request.user).first()
collection_item = CollectionItem.objects.get(
collection=collection, variant__id=variant_id
)
collection_item.quantity += 1
collection_item.save()
# ... do the rest
## In a shell to explore
>>> col = Collection.objects.first()
>>> col.collectionitem_set.all()
<QuerySet [<CollectionItem: Weedle (1) <me4-1>, Normal, x0>, ...]>
It especially helps make new queries easier and lets you focus on the thing you want to achieve rather than diving deep into the database tables and how to make sure everything gets connected correctly every time.
On top of that basic premise, Django’s ORM is so enjoyable and part of it
comes down to the REPL shell (guess what I’ll be writing tomorrow for “r”…).
The ability to jump on a Django shell and start writing
Card.objects.filter(...) to start
querying your database and building your software on top of that is something
I enjoy a ton.
Joseph Zammit’s talk Thinking in SQL with Django in DjangoDay Copenhagen 2022 improved my understanding of the ORM-SQL relationship a lot and Django Toolbar and Kolo are great tools to debug and understand the queries your application actually makes under the hood and help you identify problems and optimize the queries.
Django’s ORM can be used on a more standalone way as well to connect to query
existing databases that are not managed by Django.
Paulo Melchiorre has a wonderful blog post about it. You can write a tiny manage.py and
run a couple of commands against existing database to create
models.py and then start up a Django
shell and make queries to explore that database. Really handy when you’re
joining a new non-Django project and want a familiar toolset to explore the
database.
I know there are people who don’t like ORMs and if you’re one of those people, I invite you to write a rebuttal on your own blog and send me a webmention/email/toot. I genuinely would love to read your thoughts and update this post with links for opposing views.
If something above resonated with you, let's start a discussion about it! Email me at juhis@hamatti.org and share your thoughts. This year, I want to have more deeper discussions with people from around the world and I'd love if you'd be part of that.