#Django#Web-Development#Python#Database

Implementing Soft Delete in Django: An Intuitive Guide

Medium • Tomisin Abiodun
Medium • Tomisin Abiodun
Jan 4

Learn how Django models could implement soft delete, without modifying the interfaces and contracts of models and querysets.

Photo by Markus Winkler on Unsplash

Originally published on my blog.

In contemporary architecture, or by design requirements, we often have the need to keep certain records even after they’ve been “deleted” by the user — that’s where soft deletion comes in.

In this article, we would learn how to implement soft delete, while our application maintains its Django-ness (for lack of a better word), and we can still maintain the same interface with our model objects in the same way as we would, if we had hard deletion in place.

What is Soft Delete

Soft delete is the process of deleting records in such a way that it is still present in the database while made unavailable to the user. It involves using a flag to mark the record as unavailable, rather than deleting the record from the database.

Benefits of Soft Delete

  1. Records are easier to restore.
  2. Soft delete operations are faster because we run an UPDATE command in the database. For hard delete, we will run a DELETE command which is generally slower, especially when there are related records across tables.
  3. It keeps are related records in-place.
  4. It is the safer option, in cases where users may need to retrieve deleted information.

Cons of Soft Delete

  1. Added complexity of filtering out records that have been flagged as deleted.
  2. Unique constraints need to be refactored to include the column representing the delete flag, so records which have been soft deleted could be recreated.
  3. In some cases, records might need some unique implementations when soft deleted. For example, when soft deleting card information or payment methods, we might need to clear some sensitive fields like PAN or CVV.

Some of these fetch queries will start to look like

SELECT * FROM users WHERE is_deleted=false;

Unique constraints would also start to look this, with soft deletion in place:

CREATE UNIQUE INDEX "users_unique" ON users(is_deleted, email) WHERE is_deleted=false;

Implementing Soft Delete in Django

When implementing soft delete in Django, the first thing that’s required is a base model. This would reduce the complexity and help eliminate the aforementioned cons, as we wouldn’t need to duplicate the soft delete logic in all our models. All we would need to do, is to extend from this BaseModel.

class BaseModel(models.Model):
class Meta:
abstract = True

# common fields go here

The first thing we want to add to our base model, is the delete flag. The is_deleted field will inform us about the availability of the record.

class BaseModel(models.Model):
# ...omitted for brevity...

is_deleted = models.BooleanField(default=False)

Next, we override the delete method of the model. So, we can change the default behaviour of our model’s delete from hard deletion to soft deletion.

class BaseModel(models.Model):
# ...omitted for brevity...

def delete(self):
"""Mark the record as deleted instead of deleting it"""

self.is_deleted = True
self.save()

With this piece of code in place, we could soft delete records as we would normally hard delete records, and maintain a Django-ic codebase.

class User(BaseModel):
class Meta:
unique_together = ['email', 'is_deleted']

name = models.CharField(max_length=24)
email = models.EmailField()

new_user = User(name="Bolaji", email="user.test@example.com")
new_user.save()

print(User.objects.all()
# <QuerySet [<User: Bolaji>]>

new_user.delete()

print(new_user.is_deleted)
# True

print(User.objects.all())
# <QuerySet []>

Now that we can soft delete easily, we have to make sure that our query results never actually include any deleted record. We do so by creating a custom manager which filters out the deleted.

from django.db.models import Manager, QuerySet


class AppManager(Manager):
def get_queryset(self):
return QuerySet(self.model, using=self._db).exclude(is_deleted=True)


class BaseModel(model.Model):
# ...omitted for brevity...

objects = AppManager()

With this in place, we can replace the default objects field of our Model with our AppManager. This would ensure that when we query using the familiar syntax, we are getting expected

class User(BaseModel):
# ...omitted for brevity...


user.objects.filter(...)
user.objects.all()
# queries now exclude records flagged as deleted.

Wowza! Now, you can soft delete records while maintaining Django-ic code. Or, maybe there’s one more thing…

It’s possible to invoke delete on a queryset, instead of invoking it on an object. We have already catered for the latter my having a custom delete method in our BaseModel. To cater for the former scenario, we can make a custom queryset class.

from django.db.models import Manager, QuerySet


class AppQuerySet(QuerySet):
def delete(self):
self.update(is_deleted=True)


class AppManager(Manager):
def get_queryset(self):
return AppQuerySet(self.model, using=self._db).exclude(is_deleted=True)

Putting this altogether, we have:

from django.db.models import Manager, QuerySet


class AppQuerySet(QuerySet):
def delete(self):
self.update(is_deleted=True)


class AppManager(Manager):
def get_queryset(self):
return AppQuerySet(self.model, using=self._db).exclude(is_deleted=True)


class BaseModel(models.Model):
class Meta:
abstract = True

is_deleted = models.BooleanField(default=False)

def delete(self):
self.is_deleted = True
self.save()


# Sample model
class Post(BaseModel):
content = models.CharField(max_length=140)

def __str__(self):
return self.content


# Create a post
post = Post(content="Hello World")
post.save()

# Get all posts
print(Post.objects.all())
# <QuerySet [<Post: Hello World>]>

# Delete the post
post.delete()

print(Post.objects.all())
# <QuerySet []>

Finally, you have soft delete implemented, and you didn’t have to change your contract. Thanks for reading, buddy!

Leave a clap, share this article and do subscribe, if you found this article helpful.

Subscribe * Tomisin Abiodun