Little Brain
Little Brain

Reputation: 2837

Django: How to delete any foreign key object that is no longer referenced

I have nested data in my Django Rest Framework app, something like this:

class Student(models.Model):
    studyGroup = models.ForeignKey(StudyGroup, on_delete=models.SET_NULL,  blank=True, null=True, related_name='student')

Each student may have a study group; a student may have no study group.

Many students can have the same study group.

I would like to automatically delete any StudyGroup that is not referenced by any students, either because the student was deleted or because it was updated.

I think this can be done by customising the 'save' and 'delete' methods for Student, checking whether their StudyGroup is referenced by any other Student, and deleting it if is not referenced. Or perhaps more elegantly by using signals. But it feels like there should be a simpler way to do this - like an inverse of on_delete=models.CASCADE.

Is there a way to tell the database to do this automatically? Or do I need to write the custom code?

Upvotes: 4

Views: 2628

Answers (1)

willeM_ Van Onsem
willeM_ Van Onsem

Reputation: 476574

You can remove the StudyGroup objects that are no longer referenced by a Student with the following query:

StudyGroup.objects.filter(students__isnull=True).delete()

(this given the related_name= parameter [Django-doc] of your ForeignKey [Django-doc] is set to 'students', since this is the name of the relation in reverse).

Depending on the database backend, you could implement a trigger that can perform certain actions, for example when you remove/update an Student record. But that is backend-specific.

We can add a trigger to the Student model to remove the StudyGroups without a Student when we delete or save Students:

# app/signals.py

from app.models import Student
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver

@receiver([post_delete, post_save], sender=Student)
def update_delete_student(sender, instance, **kwargs):
    StudyGroup.objects.filter(students__isnull=True).delete()

You will need to import the signals module in your application config:

# app/app.py

from django.apps import AppConfig

class MyAppConfig(AppConfig):

    # ...

    def ready(self):
        import app.signals

But there are ways to bypass the Django signals through the ORM. For examply by using QuerySet.update [Django-doc].

Therefore it might be useful to run the method periodically, for example each day/hour. We can use celery for that [realpython] or django-periodically [GitHub].

That being said, it might not be per se the most necessary to remove the StudyGroups anyway. If you for example want to retrieve a QuerySet of StudyGroups that have at least one student, we can write this like:

# StudyGroups with at least one Student
StudyGroup.objects.filter(student__isnull=False).distinct()

So instead of removing the StudyGroups, you might decide not to show these StudyGroups, like a soft delete [wiktionary]. Then you can still recover the data later on, this of course depends on the use case.

Note: the related_name of a ForeignKey is the name of the relation in reverse, so the name of the attribute of a StudyGroup to retrieve the QuerySet of Students. Therefore naming this 'studyGroup' is a bit "weird". It would also easily result in collisions if there are two or more ForeignKeys that point to StudyGroup with the same name.

Upvotes: 4

Related Questions