In this repository, I try and explore different ways of doing soft delete in django, either using a library or from the models directly.
The reason I am trying out all these options is to make sure that the consequences of any framework or approach are well known before making a choice.
Approaches
- Paranoia model
- Django safe delete
For all the strategies we will see how the following will work
- Get
- Delete
- Queryset get and delete.
- Relations
I found this code from sentry
The idea here is to create a custom model manager which includes a custom queryset.
We create ParanoiaModel
, which will serve as the base model.
class ParanoidModel(models.Model):
class Meta:
abstract = True
deleted_on = models.DateTimeField(null=True, blank=True)
def delete(self):
self.deleted_on=timezone.now()
self.save()
Any model which needs safe delete can inherit this base model. This works well only when an individual object has to be deleted. But would fail, when delete is issued on a queryset.
So we add a custom queryset ParanoidQuerySet
.
class ParanoidQuerySet(QuerySet):
"""
Prevents objects from being hard-deleted. Instead, sets the
``date_deleted``, effectively soft-deleting the object.
"""
def delete(self):
for obj in self:
obj.deleted_on=timezone.now()
obj.save()
class ParanoidManager(models.Manager):
"""
Only exposes objects that have NOT been soft-deleted.
"""
def get_queryset(self):
return ParanoidQuerySet(self.model, using=self._db).filter(
deleted_on__isnull=True)
class ParanoidModel(models.Model):
class Meta:
abstract = True
deleted_on = models.DateTimeField(null=True, blank=True)
objects = ParanoidManager()
original_objects = models.Manager()
def delete(self):
self.deleted_on=timezone.now()
self.save()
We also add a custom manager. It helps us in 2 ways. We can access the original manager, which will return the soft deleted objects and second, queries that return queryset will filter the soft deleted objects without the need for us to specify it in each query.
Now both of the following queries work
class Post(ParanoidModel):
title = models.CharField(max_length=100)
content = models.TextField()
post = Post(title="soft delete strategies", content="Trying out various soft delete strategies")
post.delete()
# Will soft delete the post
Post.objects.all().delete()
# Will also soft delete all the posts.
Post.objects.get()
# Will not return any post and will raise an exception.
Post.original_objects.get()
# Will return the soft deleted post.
Post.original_objects.all()
# Returns soft deleted objects as well, along with the undeleted ones.
This strategy works very well for the first 3 criterion. But how does this work across relations ?
Lets add another model to the above example
post = Post(title="soft delete strategies", content="Trying out various soft delete strategies")
class Comment(ParanoidModel):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
message = models.TextField()
comment = Comment(post, message="Well written blog post")
# post is the object we created earlier.
post.delete()
# Soft delete the post.
print(Comment.objects.count())
# The comment of the post still exists.
From the above example it is clear that the soft delete is not propagated to the relations. Deleting a post does not delete the comments related to it. They still can be accessed independently, but cannot be accessed from the post, since the post is soft deleted.
So summarising this approach, everything works well, other than the relations handling.
This implementation is good enough, if the relation models are not queried directly. For example, once
we delete the post, the comments related to the post become irrelevant. Comments don't mean a thing
without its parent post
.
The other thing to note here is, if we decide to restore a soft deleted object, we don't have to worry about its relations, since they have not been deleted (Neither soft/hard).
If you want restore option, you can add undelete
method to the base model ParanoiaModel
.
class ParanoidModel(models.Model):
class Meta:
abstract = True
deleted_on = models.DateTimeField(null=True, blank=True)
objects = ParanoidManager()
original_objects = models.Manager()
def delete(self):
self.deleted_on=timezone.now()
self.save()
def undelete(self):
self.deleted_on=None
self.save()
You can also add this to the custom queryset.
class ParanoidQuerySet(QuerySet):
"""
Prevents objects from being hard-deleted. Instead, sets the
``date_deleted``, effectively soft-deleting the object.
"""
def delete(self):
for obj in self:
obj.deleted_on=timezone.now()
obj.save()
def undelete(self):
for obj in self:
obj.deleted_on=None
obj.save()
Note: I've made some changes to the code found from sentry, like changing the field name deleted_on
This framework provides lot of options for soft deleting. They have the following policies
- HARD_DELETE
- SOFT_DELETE
- SOFT_DELETE_CASCADE
- HARD_DELETE_NOCASCADE
- NO_DELETE
Policies apply to how the delete is handled and stored in the database.
They have the following visibility options
- DELETED_INVISIBLE (Default)
- DELETED_VISIBLE_BY_FIELD
Visibility options apply for retrieving data.
I will focus only on the soft delete policies, as other options are not relevant. You can check out the documentation if you are interested. doc
This is similar to the django default behaviour, with some more options. I am not going to discuss them here. You can checkout the documentation here.
This policy just soft deletes the object being deleted. The related objects remain untouched.
Lets start by creating some models
from django.db import models
from safedelete.models import SafeDeleteModel
from safedelete.models import SOFT_DELETE
class Article(SafeDeleteModel):
_safedelete_policy = SOFT_DELETE
title = models.CharField(max_length=100)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Comment(SafeDeleteModel):
_safedelete_policy = SOFT_DELETE
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='article_comments')
text = models.TextField()
Lets try out deleting an article
# First we create an article
article = Article.objects.create(
title="article 1 title",
content="article 1 content"
)
article.delete()
# Will soft delete the article.
Article.objects.all().delete()
# Will soft delete all the articles.
Article.objects.all()
# Will return objects which are not deleted (Either soft/hard)
Article.objects.all_with_deleted()
# Will fetch all the objects including the deleted one's
Article.original_objects.all()
# Will fetch all the objects including the deleted one's using our custom manager.
We can restore the soft deleted object
article.undelete()
In this approach, firt 3 criterion work well, but fails for relations. So soft deleting an objects, doesn't soft delete its relations.
This strategy is almost similar to the paranoia design we discussed above.
This is almost similar to the above, except that it soft delete's the related objects as well.
Let's start by creating some models
from django.db import models
from safedelete.models import SafeDeleteModel
from safedelete.models import SOFT_DELETE_CASCADE
class User(SafeDeleteModel):
_safedelete_policy = SOFT_DELETE_CASCADE
full_name = models.CharField(max_length=100)
email = models.CharField(max_length=100)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class UserLogin(SafeDeleteModel):
_safedelete_policy = SOFT_DELETE_CASCADE
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="user_logins")
login_time = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
Lets try deleting the user
user = User.objects.create(
full_name="sam kin",
email="[email protected]"
)
UserLogin.objects.create(
user=user
)
UserLogin.objects.create(
user=user
)
user.delete()
User.objects.count()
# User count will be 0
UserLogin.objects.count()
# UserLogin count will also be 0. (Since this is cascade soft delete)
# Both user and user login are soft deleted.
Here, restoring user will also restore all its login's. So all the related objects are also restored.
user.undelete()
This approach handles all our criterions.
This policy prevents any sort of delete soft/hard. The only way to delete is through raw sql query. This can be useful in places where any kind of delete is not allowed from the application.
All the approaches fall under 2 categories
- Supports relations
- Doesn't support relations
If you want your soft-delete's to be propogated to the relations use soft-delete-cascade. If this is not required, then you can choose any of the above approaches.