django
February 6, 2021

Django Soft-Delete

Soft delete" in database lingo means that you set a flag on an existing table which indicates that a record has been deleted, instead of actually deleting the record.

Let's say you want to implement soft-delete for a model defined in a django application. Let's imagine you have a model Item:

from django.db import models

class Item(models.Model):
    title = models.CharField(max_length=255)
    content = models.TextField()

Now you want to add ability to use soft delete for this model. First we need to add a field that will tell us if our Item is deleted. We have at least 2 options here: we can add a boolean is_deleted field or a datetime deleted_at field. We will use the latter in this example. Let's create an abstract model for simplicity:

from django.db import models
from django.utils import timezone

class SoftDeleteModel(models.Model):
    deleted_at = models.DateTimeField(blank=True, null=True)
    
    def delete(self, *args, **kwargs):
        self.deleted_at = timezone.now()
        self.save()
    
    def restore(self):
        self.deleted_at = None
        self.save()
    
    def hard_delete(self, *args, **kwargs):
        super(SoftDeleteModel, self).delete(*args, **kwargs)
        
    class Meta:
        abstract = True

Next, we need to implement a custom queryset object, so we can do same operations with querysets:

from django.utils import timezone
from django.db.models.query import QuerySet


class SoftDeleteQuerySet(QuerySet):
    def delete(self):
        return super(SoftDeleteQuerySet, self).update(deleted_at=timezone.now())
        
    def restore(self):
        return super(SoftDeleteQuerySet, self).update(deleted_at=None)
        
    def hard_delete(self):
        return super(SoftDeleteQuerySet, self).delete()

    def not_deleted(self):
        return self.filter(deleted_at=None)
        
    def deleted(self):
        return self.exclude(deleted_at=None)

Quite easy. Now we need to define a custom object manager and use queryset that we created in previous code block:

from django.db import models
from softdelete.querysets import SoftDeleteQuerySet


class SoftDeleteManager(models.Manager):
    def __init__(self, *args, **kwargs):
        self.all_objects = kwargs.pop('all_objects', False)
        super(SoftDeleteManager, self).__init__(*args, **kwargs)
    
    def get_queryset(self):
        if self.all_objects:
            return SoftDeleteQuerySet(self.model)
        return SoftDeleteQuerySet(self.model).not_deleted()
    
    def hard_delete(self):
        return self.get_queryset().hard_delete()

As a final change we just change our Item model so it inherits from SoftDeleteModel, and it's done!

from softdelete.models import SoftDeleteModel

class BlogPost(SoftDeleteModel):
    title = models.CharField(max_length=255)
    content = models.TextField()

Voila!