In data-driven business applications, deleting records from the database is rarely a good idea. Accidental clicks can lead to catastrophic data loss, and hard-deleting records breaks historical audits and referential integrity. The industry standard is Soft Deletion—marking records as "deleted" via a timestamp column instead of actually removing the rows.
While there are several third-party Django libraries (like django-safedelete) that handle soft deletion, they often come with heavy overhead, buggy database migrations, and complex behaviors when performing cascading deletes or object serialization. In this post, we'll explore how to build a lightweight, bulletproof Soft Delete system from scratch using custom Django Managers and QuerySets.
The System Architecture: Three Simple Components
To build a native soft-delete system in Django, we need to customize three primary layers:
- Custom QuerySet: Ensures that bulk operations (like
.delete()and.restore()) update the timestamp column instead of dropping the rows. - Custom Managers: Defines how models query records by default (filtering out deleted items) while providing raw entry points for trash administration.
- Abstract Model: Serves as the base model containing the
deleted_attimestamp and overriding the defaultdelete()method. - ORM Compatibility: Overriding bulk
.delete()inside theQuerySetguarantees that bulk executions (e.g.Product.objects.filter(price__gt=100).delete()) perform soft deletions correctly. - Referential Integrity: Using an abstract class allows you to maintain foreign keys, avoiding complex database cascade configurations.
- Lightweight Footprint: Zero external third-party dependencies! This guarantees smooth database migrations and full compatibility with future Django versions.
Step 1: Custom QuerySets and Managers
We'll create a SoftDeleteQuerySet that overrides the bulk .delete() call to execute an .update() statement. We'll also define a DeletedObjectQuerySet with a custom .restore() utility:
from django.db import models
from django.utils.timezone import now
class SoftDeleteQuerySet(models.QuerySet):
def delete(self):
# Override bulk delete to set timestamp
return self.update(deleted_at=now())
class DeletedObjectQuerySet(models.QuerySet):
def restore(self):
# Bulk restore
return self.update(deleted_at=None)
class SoftDeleteManager(models.Manager):
def get_queryset(self):
# Default manager only queries active records
return SoftDeleteQuerySet(self.model, using=self._db).filter(deleted_at__isnull=True)
class DeletedObjectManager(models.Manager):
def get_queryset(self):
# Queries ONLY deleted items (the trash bin)
return DeletedObjectQuerySet(self.model, using=self._db).filter(deleted_at__isnull=False)
Step 2: Building the Abstract Base Model
Next, we construct the abstract model. We define three managers: the default objects (active items only), deleted_objects (deleted items), and all_objects (a raw, unfiltered manager representing the entire physical table):
class SoftDeleteModel(models.Model):
deleted_at = models.DateTimeField(blank=True, null=True, default=None)
# Register custom managers
objects = SoftDeleteManager()
deleted_objects = DeletedObjectManager()
all_objects = models.Manager() # Native raw manager
class Meta:
abstract = True
def delete(self, *args, **kwargs):
# Single instance soft delete
self.deleted_at = now()
self.save(update_fields=['deleted_at'])
def restore(self):
# Restore instance
self.deleted_at = None
self.save(update_fields=['deleted_at'])
@property
def is_deleted(self):
return self.deleted_at is not None
Step 3: Integrating Soft Deletes in Your Code
Using the soft-delete model is incredibly simple. Just inherit from SoftDeleteModel when writing your tables, and Django handles the filtering transparently:
class Product(SoftDeleteModel):
name = models.CharField(max_length=100)
price = models.DecimalField(max_digits=10, decimal_places=2)
# Queries:
# 1. Fetching active products (automatically excludes deleted ones)
active_products = Product.objects.all()
# 2. Soft-deleting a product
product = Product.objects.first()
product.delete() # Simply sets product.deleted_at = now()
# 3. Viewing the trash bin
trash_bin = Product.deleted_objects.all()
# 4. Restoring a product
deleted_product = Product.deleted_objects.first()
deleted_product.restore() # Sets deleted_at = None
Key Implementation Advantages
Building your own soft-delete framework in Django requires less than 50 lines of clean Python code, providing a secure, performant, and maintainable trash bin system for your enterprise datasets.