Soft Deletion in Django
Giving your users the ability to delete objects from your database is a risky proposition — some types of objects are low risk, but others, no matter how much you warn them of the risks of deleting things, will lead to customer requests. You know the kind — “So, I deleted this thing, but I didn’t mean to and now I really need it back…can you recover it?” Of course, databases don’t work that way. When a thing is deleted, it’s deleted.
So, the dream: be able to easily define some objects to be soft-deleted and others to be hard-deleted so that your developers don’t have to remember as they are interacting with objects which are which, and be able to easily recover deleted objects for your users when they need you to. Soft deletion can help with this.
Most objects in Django inherit from
models.Model. If we define a
SoftDeletionModel that our objects inherit from instead, we can give it whatever attributes we want, and trust that all models that inherit this way will have the same behavior, and we only have to remember it at the time we define the model, not every time we use it. It might look something like this:
deleted_at = models.DateTimeField(blank=True, null=True)
objects = SoftDeletionManager()
all_objects = SoftDeletionManager(alive_only=False)
abstract = True
self.deleted_at = timezone.now()
deleted_at: this means that all models inheriting from the
SoftDeletionModelwill have this attribute available to be set. By default it will be null. I recommend a date instead of a boolean so that you can create a background job that hard-deletes any objects that were “deleted” more than 24 hours/7 days/30 days (whatever the right cadence is for you and your users ) ago — data that users choose to delete should actually be deleted.
- We’ll look at
all_objectsin the next section. This is what makes this so powerful
abstract = True: This just means we won’t ever define a
SoftDeletionModelobject on its own. More detail from the Django docs here.
deletemethod means that whenever you call
.delete()on any object that inherits from the
SoftDeletionModel, it won’t actually be deleted from the database — its
deleted_atattribute will be set instead
hard_deletegives you the option to really truly delete something from the database if you want to, but is named something other than the usual delete methods to ensure that you have to think about what you’re doing before you do it, and actually mean to do it. This usually won’t be exposed to users, but could only be called by developers from the shell.
all_objects attributes defined on the
SoftDeletionModel above reference a Django custom manager.
def __init__(self, *args, **kwargs):
self.alive_only = kwargs.pop('alive_only', True)
super(SoftDeletionManager, self).__init__(*args, **kwargs)
- We initialize with
alive_onlyset to True by default, unless we’ve instantiated the manager with that in the
- We define
get_querysetthat, unless we’re calling
all_objectsreturns any object that doesn’t have a value for
deleted_at— you don’t want to be working with things your users think they’ve deleted! Otherwise, just return everything, using the
SoftDeletionQuerySet, which we’ll look at below
hard_delete, once again allows us to really truly delete a thing.
Before we jump in, here are the docs on Django QuerySets.
return super(SoftDeletionQuerySet, self).update(deleted_at=timezone.now())
return super(SoftDeletionQuerySet, self).delete()
delete— bulk deleting a QuerySet bypasses an individual object’s delete method, which is why this is needed here as well
deadare just helpers — you may find you don’t need them.
hard_delete, as above, actually removes the objects from your database, but does this on a QuerySet instead of an individual object
Putting it all together
Lets say we’ve got a
VeryImportantSomething that we want users to think they can delete, but that we want to be able to recover for them in case they do it, and come back with regrets.
# Define the model just as you would any other Django model
- If I call
VeryImportantSomething.objects.get(pk=123).delete(), I will not remove the object from the database, but instead set the
- If I call
VeryImportantSomething.objects.all()I will actually get all
VeryImportantSomethingsthat do not have a value set on their
deleted_atattribute. Likewise, if I call
VeryImportantSomething.objects.get(pk=123), I will get an
ObjectDoesNotExisterror, as if it weren’t in my database at all.
- If I were to call
VeryImportantSomething.all_objects.get(pk=123), however, the object would be returned to me (and I could then set
None, and thereby “un-delete” it for my user!