Django Abstract Models

Django Abstract Models

Fighting the DRY battles

Image credit Unsplash

Scenario 1

Fat models, skinny views

Handle your business logic in models

These are what are considered MVC best practices when it comes to writing Model-View-Controller architectures.

But as your codebase grows from 10 lines per file to 20 to 500, to IDK, it gets harder to maintain, or fix issues/bugs that may(will) arise. This where you segregate your models by different logic as they come.

Scenario 2

Assuming you have 10 model classes, and some fields must be present and handled across all the classes/tables. This is you throwing DRY to the pit.

In solving scenarios 1 and 2 and of course subsequent scenarios that may arise from your models, Django provides a solution called Abstract models

The best way to explain Abstract models is by writing code, yeah? So let's go

I will skip the prerequisites of setting up a Django project and move on to what this is about.

Our task

We want to create two tables, A and B, that both have timestamp fields(created_at, updated_at, deleted_at, etc) and some activity fields like updated_by, created_by etc.

We can easily add the fields to each class A and B and move on with our lives, but what if we have hundreds of tables/model classes that require all these activity fields? That's where Abstract models come in.

So let's code :)

from django.db import models

class AbstractModel(models.Model):
    created_at = models.DateTimeField(auto_add_now=True)
    updated_at = models.DateTimeField(auto_add=True)
    deleted_on = models.DateTimeField(null=True, blank=True) # This can be used for soft delete
    created_by = models.ForeignKey(User, on_delete=models.DO_NOTHING)
    updated_by = models.ForeignKey(User, on_delete=models.DO_NOTHING, null=True, blank=True)

    class Meta:
        abstract = True # This is the most important aspect of this class

    # You other universal logic/methods can go here

    # This is for soft delete
    def delete(self):
        self.deleted_on = timezone.now()
        self.save(updated_fields = ['deleted_on'])

    def hard_delete(self):
        super(AbstractModel, self).delete()

    # You can also override the save method 
    def save(self, *args, **kwargs):
        user = kwargs.get("user")
        if self.pk:  # Object already exists
             self.updated_by = user # This can be passed from maybe your views
        else:
             self.created_by = user # This can be passed from maybe your views
        super(AbstractModel, self).save(*args, **kwargs)

We have created an abstract model that can be used in all our models/tables and we won't need to re-write most of this global logic again

What is the effect of this on migration? This won't create a new table in our database, it does not affect our database until it is inherited by a proper model class, as it will in the code below

class A(AbstractModel):
      house = models.CharField(max_length=255)
      occupants = models.IntegerField()

class B(AbstractModel):
      school = models.CharField(max_length=255)
      students = models.IntegerField()

After migrating this will create two tables in our database namely, appname_A and appname_B( Note: appname here is a placeholder for the name of your app, override Meta.db_name for each class.

Table A

created_atcreated_byupdated_atupdated_bydeleted_onhouseoccupants

Table B

created_atcreated_byupdated_atupdated_bydeleted_onschoolstudents
A sample usage
def create(request):
    house = "Detached"
    occupants = 4
    a = A(house=house, occupants=occupants)
    a.save(user=request.user)

Updated Table A

Table A

created_atcreated_byupdated_atupdated_bydeleted_onhouseoccupants
datetimeuser_iddatetimenullnullDetached4

With these, we have removed abstractions and repetitions from our models. Technically our model is fat, but it has been skinned.

Feel free to drop your comments. Till we meet again, stay jiggy