Inspired by a post on override the default model manager to support soft-deleting objects in the database, I decided to implement it into one of my projects. (Yes, I know it’s an old post but it was new to me!).
However, just being able to filter the query sets returned to exclude items which had deleted=true wasn’t enough for what I wanted to do. Nor was just customizing the django’s admin interface to filter by on whether or not deleted was true or false. I wanted the modified manager and objects to soft-delete all related objects (models that referenced the object-being-deleted via a ForeignKey).
Thus, the django-softdelete project was born.
The code to handle soft-delete was made into an abstract base class that concrete models can inherit from; thereby giving them the deleted field as well as setting derived classes to use the SoftDeleteQuerySet model manager to act as their model manager.
Once I added the cascading soft-delete, I decided I also wanted an undelete operation that would cascade in a similar fashion. But just naively undeleting all related objects wouldn’t provide the level of data integrity I was looking for. I needed some way to track the objects that were part of a cascading soft-delete. So I came up with the ChangeSet model.
class ChangeSet(models.Model): created_date = models.DateTimeField(default=datetime.utcnow) content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() record = generic.GenericForeignKey('content_type', 'object_id')
The ChangeSet links to the “master” object being soft-deleted with a GenericForeignKey.
A ChangeSet encompasses the total cascading soft-delete. To accomplish this, each time an object is soft-deleted a SoftDeleteRecord is created. Like the ChangeSet, the SoftDeleteRecord uses a GenericForeignKey to link to the object that this record is tracking.
class SoftDeleteRecord(models.Model): changeset = models.ForeignKey(ChangeSet, related_name='recordsets') created_date = models.DateTimeField(default=datetime.utcnow) content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() record = generic.GenericForeignKey('content_type', 'object_id')
When you soft-delete an object, any objects referencing it via a ForeignKey, ManyToManyField, or OneToOneField will also be soft-deleted. This mimics the traditional CASCADE behavior of a SQL DELETE.
When the soft-delete is performed, the system makes a ChangeSet object which tracks all affected objects of this delete request. Later, when an undelete is requested, this ChangeSet is referenced to do a cascading undelete.
If you are undeleting an object that was part of a ChangeSet, that entire ChangeSet is undeleted.
Once undeleted, the ChangeSet object is removed from the underlying database with a regular (“hard”) delete.
You can get access to the code at the git repository.