Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Force a cascading delete for a Django model

I have a Django model with foreign-key relations that are marked as deletion.PROTECT, and I am OK with that behavior, since it's how the model should behave in most scenarios.

However, there is one use case for those models where I kind of need to do a "hard delete" (ie user wants to delete their account). In that case, I'd really like everything to behave like a CASCADE, instead of having to delete each of the foreign-key relationships manually. Is there a way to do this cleanly? In an ideal world, the model.delete() call would take a parameter that is something like force_cascade=True.

like image 994
Octodone Avatar asked Oct 28 '25 08:10

Octodone


1 Answers

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 protected]')
    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.

like image 135
Kound Avatar answered Oct 31 '25 03:10

Kound