Eliminate boredom and reduce tension with Abstract models in Django
In this article, I will show you how to leverage inheritance in your Django models to ensure elegant, clean and neat code.
Working with Django, we get lost in love, wonder and praise as we discover neat stuffs we can work with, one of such being abstract models.
I've come to use UUIDs in all my django models by default. With time, I've wanted to make things easier for myself and try to leverage abstraction in OOP to ensure my models could automatically have uuid fields without having to explicitly specify them.
As usual, we'll begin with a problem:
The problem
Isaac from my previous article who as usual loves shady deals has been leveraged with a small project from LEFTR (LEague of Fundamental Thieves and Robbers). They want a managed record of their members who have been caught by the law and the police agent involved in bringing them to book. They call the project, Ole.
Isaac then churned the following elegant code in the models.py of Ole project:
import uuid
from django.db import models
from django.template.defaultfilters import slugify
class Police(models.Model):
uuid = models.UUIDField(default=uuid.uuid4)
first_name = models.CharField(max_length=32)
last_name = models.CharField(max_length=32)
nickname = models.CharField(max_length=32)
avatar = models.ImageField(upload_to='police')
slug = models.Slugfield(blank=True)
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.nickname
def save(self, *args, **kwargs):
identifier = f"{self.first_name} {self.last_name}"
self.slug = slugify(identifier)
return super().save(*args, **kwargs)
class Thief(models.Model):
uuid = models.UUIDField(default=uuid.uuid4)
first_name = models.CharField(max_length=32)
last_name = models.CharField(max_length=32)
street_name = models.CharField(max_length=32)
avatar = models.ImageField(upload_to='thief')
slug = models.Slugfield(blank=True)
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.street_name
def save(self, *args, **kwargs):
identifier = f"{self.first_name} {self.last_name}"
self.slug = slugify(identifier)
return super().save(*args, **kwargs)
That wasn't too bad was it?
The issue
Isaac feels this code should be improved since it's his first real project. In addition, he might never make it outside the room he'll present the code in, to the thieves. So, he's particular about not dying while decrepit code stays on his Github account.
The actual issue
As you can see, there are lots of fields that both Thief and Police share. Normally, under OOP circumstances, you'd be reeling with "Why isn't Isaac doing some inheritance here?". Thing is, we're dealing with databases here and this is Django and not largely-unopinionated frameworks like Express or Flask where you can likely do what you want. This means you've got to do it in an intuitive and Django-friendly manner.
How to do?
Here comes abstract models. What we'll do is to extract common functionality into a model and tag it as abstract, meaning it is not concrete enough to be a table in the database but it's solid enough to be a structure for other tables.
So, Isaac defines a BaseModel
like so:
class BaseModel(models.Model):
uuid = models.UUIDField(default=uuid.uuid4)
first_name = models.CharField(max_length=32)
last_name = models.CharField(max_length=32)
slug = models.Slugfield(blank=True)
timestamp = models.DateTimeField(auto_now_add=True)
def save(self, *args, **kwargs):
identifier = f"{self.first_name} {self.last_name}"
self.slug = slugify(identifier)
return super().save(*args, **kwargs)
class Meta:
abstract = True
And then reworks the other models to look like so:
class Police(BaseModel):
nickname = models.CharField(max_length=32)
avatar = models.ImageField(upload_to='police')
def __str__(self):
return self.nickname
class Thief(BaseModel):
street_name = models.CharField(max_length=32)
avatar = models.ImageField(upload_to='thief')
def __str__(self):
return self.street_name
Let's explain
The BaseModel
is basically inheritance at work. We extract common functionalities in both Thief
and Police
models and make it a parent class BaseModel
. Since we don't want a table to be created for BaseModel
, we add a Meta
subclass and add the abstract = True
attribute.
If abstract = True
is omitted, we'll be having three tables in our database when we run python manage.py makemigrations
: One for Thief
, one for Police
and one for BaseModel
and that's not what we want. There are cases where you might want that though.
As you can see, all methods of the parent class are inherited by the children classes.
I am your saviour
There, little children, you can rework your abandoned projects to look more elegant with abstract models, especially in stuffs such as UUID usage across all models or even timestamps.
May the sublime force
be with you.
Photo credits: Maksim Shutov on Unsplash