[Django]-Force a cascading delete for a Django model

5đź‘Ť

As django also creates the database with PROTECTED relations you need to do the cascading deletion yourself manually. The database itself will otherwise forbid the deletion.

Django’s ORM can help you with that, the only thing you need to do is to find recursively all references to the user and delete them in reverse order.
It is also an advantage to do this manually as you might want to replace some occurrences of the user with a substitute (i.e. a virtual "deleted user"). I could think of comments in a message board that should be kept even so if the user deletes their account.

To find the relations pointing to the current user and replace them with a ghost user, you can use the following snippet.

from typing import List
from django.contrib.auth import get_user_model
from django.db.models import Model
from django.db.models.fields.reverse_related import (
    ManyToOneRel,
    ForeignObjectRel,
)

User = get_user_model()


def get_all_relations(model: Model) -> List[ForeignObjectRel]:
    """
    Return all Many to One Relation to point to the given model
    """
    result: List[ForeignObjectRel] = []
    for field in model._meta.get_fields(include_hidden=True):
        if isinstance(field, ManyToOneRel):
            result.append(field)
    return result


def print_updated(name, number):
    """
    Simple Debug function
    """
    if number > 0:
        print(f"   Update {number} {name}")


def delete_user_and_replace_with_substitute(user_to_delete: User):
    """
    Replace all relations to user with fake replacement user
    :param user_to_delete: the user to delete
    """
    replacement_user: User = User.objects.get(pk=0)  # define your replacement user
    # replacement_user: User = User.objects.get(email='email@a.com')
    for field in get_all_relations(user_to_delete):
        field: ManyToOneRel
        target_model: Model = field.related_model
        target_field: str = field.remote_field.name
        updated: int = target_model.objects.filter(
            **{target_field: user_to_delete}
        ).update(**{target_field: replacement_user})
        print_updated(target_model._meta.verbose_name, updated)
    user_to_delete.delete()
 

For a real deletion simply replace the .update(...) function with a .delete() call (don’t forget to recursively look for protected relations before, if needed)

There might be also a postgresql related solution that I am not aware of. The given solution is database independent.

In general it is a good idea to keep every relation PROTECTED to prevent accidentally deleting important database entries and delete manually with care.

👤Kound

0đź‘Ť

models.PROTECT is a setting for the table in the database. You would have to issue raw SQL instructions to override it, and that would be database-specific (and I don’t have a clue how to do that).

The alternative is to navigate the “tree” of objects that you want to remove, and then delete objects working from the protected “leaves” inwards to the “trunk”. So if you had

class Bar( models.Model):
   user = models.ForeignKey( User, models.PROTECT, ...) 
   ...

class Foo( models.Model):
    bar = models.ForeignKey( Bar, models.PROTECT, ... )
    ...

Then to delete a user object user you would need

def delete_user( user):
    for bar in user.bar_set.all():
        bar.foo_set.all().delete()
        bar.delete()
    user.delete()

I’d wrap it in a transaction so it either deleted everything or nothing.

It will hit the DB multiple times. I’m assuming that the number of related (bar, baz) objects is fairly small and that you won’t be deleting users very often.

I have always wondered what one does if instance a has a protected foreign key relation to instance b and vice versa (maybe via intermediates). At face value this means you can create objects that are un-deleteable.

👤nigel222

Leave a comment