Codementor Events

Django soft delete options

Published Feb 27, 2019

Understanding the nuances of soft delete’s in Django


Maintain calm. Source: https://pixabay.com/en/users/Lars_Nissen_Photoart-2780243/

In this post, 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. And accidental deletion of production data is no breeze.

Approaches:

  1. Paranoia model
  2. Django safe delete https://github.com/makinacorpus/django-safedelete

For all the approaches, we will verify how the following are handled.

  1. GET
  2. DELETE
  3. Queryset GET and DELETE
  4. Relations

Paranoia model:

I found this code in 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 their 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

Django safe delete

This framework provides lot of options for soft deleting. They have the following policies

  1. HARD_DELETE
  2. SOFT_DELETE
  3. SOFT_DELETE_CASCADE
  4. HARD_DELETE_NOCASCADE
  5. NO_DELETE

Policies apply to how the delete is handled and stored in the database.

They have the following visibility options

  1. DELETED_INVISIBLE (Default)
  2. 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

HARD_DELETE

This is similar to the Django default behaviour, with some more options. I am not going to discuss them here. You can checkout their documentation.

SOFT_DELETE

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 SafeDeleteModelfrom 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()

Let’s 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, first 3 criterion work well, but fails for relations. So soft deleting an object, doesn’t soft delete its relations.

This strategy is almost similar to the paranoia design we discussed above.

SOFT_DELETE_CASCADE

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 SafeDeleteModelfrom 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)

Let’s try deleting the user

user = User.objects.create( full_name="sam kin", email="sam@gm.com")

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.

We can see that soft delete’s are propagated to the relations as well.

Here, restoring user object will restore all its login’s. So all the related objects are restored.

user.undelete()

This approach handles all our criterions.

NO_DELETE

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.

Summary

All the approaches fall under 2 categories

  1. Supports relations
  2. Doesn’t support relations

If you want your soft-delete’s to be propagated to the relations use soft-delete-cascade. If this is not required, then you can choose any of the above approaches.

You can find the code samples with tests in my Github repo https://github.com/Dineshs91/soft-delete-options-in-django/

Note: I've published the same post in medium.


Follow me on Twitter & Github

Discover and read more posts from Dinesh S
get started
post commentsBe the first to share your opinion
Show more replies