djvg
djvg

Reputation: 14255

Django: How to determine if an object is referenced by any other object?

summary

In Django, what is the simplest way to determine if there are any objects in our database that refer to a given object?

details

Consider this minimal example from Django's Related objects reference:

from django.db import models

class Reporter(models.Model):
    pass

class Article(models.Model):
    reporter = models.ForeignKey(Reporter, on_delete=models.CASCADE)

How can we determine if there are any objects, so not only Article objects, that point to a given Reporter object, either through a OneToOneField, a ForeignKey, or a ManyToManyField?

In other words, we want to determine if there are any reverse relations to the given object.

For Article it is easy, we can just get e.g. reporter.article_set.count(), but other models may be added later which also point to Reporter, and these will also have to be taken into account.

example use-case

An example use case is where we want to prevent modification as soon as an object is being referenced by any other object. Or we could use it to enforce a kind of behavior similar to the on_delete=models.PROTECT mechanism.

Upvotes: 2

Views: 1715

Answers (1)

djvg
djvg

Reputation: 14255

Here's a working solution, using the Model._meta API, but I am not sure if this is the best way to do it. Hoping for better answers.

Basically, given an object, we get a list of its reverse relations, then, for each of those, we check if there are any objects in the relation.

# just any given reporter object
obj = Reporter.objects.first()
# assume no references to obj
obj_has_reverse = False
# skip for new objects (i.e. those not yet saved to database)
if obj.id is not None:  
    # reverse relation "fields" on the Reporter model are auto-created and
    # not concrete
    for reverse in [f for f in obj._meta.get_fields() 
                    if f.auto_created and not f.concrete]:
        # in case the related name has been customized
        name = reverse.get_accessor_name()
        # one-to-one requires a special approach
        has_reverse_one_to_one = reverse.one_to_one and hasattr(obj, name)
        has_reverse_other = not reverse.one_to_one and getattr(obj, name).count()
        if has_reverse_one_to_one or has_reverse_other:
            obj_has_reverse = True

Note that a reverse relation for ForeignKey and ManyToManyField returns a RelatedManager, so we can check e.g. the count(). However, a reverse relation for OneToOneField does not return a RelatedManager. It raises a DoesNotExist exception if there is no related object, as described in the docs. Also see the source for reverse_related.

Upvotes: 1

Related Questions