Kevin Parker
Kevin Parker

Reputation: 17216

Django ORM - Soft delete objects with flag, but still be able to look them up in related queries

We've developed quick way to do soft deletes in Django by:

  1. Adding a delete_on field to models that could be 'soft deleted'
  2. Set objects = OurObjectManger in the model
  3. OurObjectManager just overrides get_queryset and appends filter(deleted_on=None) to the queryset.
  4. Calling instance.soft_delete() sets the deleted_on field

Works well in practice, Transfers are hidden when they're deleted and queries don't return them.

The only problem is we'd like these still to show up when being referenced by foreign key in another model. For example the Transaction model references Transfer such as transaction.transfer, which is now None to Django.

Any ideas?

Upvotes: 3

Views: 2557

Answers (3)

dismine
dismine

Reputation: 575

In addition to @kevin-parker's answer i want to left this code based on idea from article Soft Deletion in Django. I made changes to support polymorphic models, but did not test this code. Changed my mind to use django-polymorphic.

from django.db import models
from django.utils import timezone
from polymorphic.models import PolymorphicModel
from polymorphic.managers import PolymorphicManager
from polymorphic.query import PolymorphicQuerySet

class SoftPolymorphicQuerySet(PolymorphicQuerySet):
    def delete(self):
        return super().update(deleted_at=timezone.now())

    def hard_delete(self):
        return super().delete()

    def alive(self):
        return self.filter(deleted_at=None)

    def dead(self):
        return self.exclude(deleted_at=None)


class SoftPolymorphicDeletionManager(PolymorphicManager):
    queryset_class = SoftPolymorphicQuerySet

    def __init__(self, *args, **kwargs):
        self.alive_only = kwargs.pop('alive_only', True)
        super().__init__(*args, **kwargs)

    def get_queryset(self):
        qs = super().get_queryset()
        if self.alive_only:
            return qs.filter(deleted_at=None)
        return qs

    def hard_delete(self):
        return self.get_queryset().hard_delete()


class SoftPolymorphicDeletionModel(PolymorphicModel):
    deleted_at = models.DateTimeField(blank=True, null=True)

    class Meta(PolymorphicModel.Meta):
        abstract = True

    objects = SoftPolymorphicDeletionManager()
    all_objects = SoftPolymorphicDeletionManager(alive_only=False)

    # For backward compatibility keep original arguments
    def delete(self, *args, **kwargs):
        self.deleted_at = timezone.now()
        self.save()

    # Pass arguments to original delete method
    def hard_delete(self, *args, **kwargs):
        super().delete(*args, **kwargs)

Upvotes: 0

Kevin Parker
Kevin Parker

Reputation: 17216

We ended up fixing this, just call objects_soft when we want only the soft objects... This way related lookups already use the default object manager and find related fk's. For our polymorphic models this ends up being:

objects = PolymorphicManager()
objects_soft = SoftPolymorphicObjectManager()

Query them using

TransferBase.objects_soft.filter()...

Where the model extends soft delete functions:

class SoftDeleteFunctions(object):
    def soft_delete(self):
        self.deleted_on = now()
        self.save()

class SoftPolymorphicObjectManager(PolymorphicManager):
    def get_queryset(self):
        queryset = self.queryset_class(self.model, using=self._db)
        return queryset.filter(deleted_on=None)

class SoftObjectManager(Manager):
    def get_queryset(self):
        queryset = super(Manager, self).get_queryset()
        return queryset.filter(deleted_on=None)

Upvotes: 0

theWanderer4865
theWanderer4865

Reputation: 871

As long as the manager doesn't have:

use_for_related_fields = True

as a class attribute, the foreign key lookup

transaction.transfer

will still populate a model instance if it exists.

The other thing to note here is that you should be aware that deleting model objects that reference or are referenced by other models will need to be handled delicately so you don't lose any data accidentally when calling delete.

e.g.

The delete normally works like:

Transfer.objects.filter(id=my_id).delete() --> Deletes all other objects related to it by default, sets to null if you like

But if you added soft delete, that is application level logic that makes it so if you do:

Transaction.objects.filter(id=another_id).delete()

You may unintentionally delete the objects you didn't want to delete!

If the non-soft-delete models know that deleting is inappropriate, you should be okay (the set null behavior, overriding the delete method on the model class and model manager).

For reference, see how it was done with pinax-models

Docs for related managers

Upvotes: 2

Related Questions