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_at | created_by | updated_at | updated_by | deleted_on | house | occupants |
Table B
created_at | created_by | updated_at | updated_by | deleted_on | school | students |
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_at | created_by | updated_at | updated_by | deleted_on | house | occupants |
datetime | user_id | datetime | null | null | Detached | 4 |
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