2

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.

djvg
  • 11,722
  • 5
  • 72
  • 103
  • Hey, have a look at the `contenttype` framework: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/. A call like `ContentType.objects.all()` should allow you to retrieve all the models in your system. You can build from there... – Giacomo Casoni Feb 15 '19 at 14:47
  • Somewhat similar, but does not answer the question: https://stackoverflow.com/q/19512187 – djvg Feb 15 '19 at 15:17

1 Answers1

1

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.

djvg
  • 11,722
  • 5
  • 72
  • 103
  • 1
    you probably meant to change `Terms` to `Reporter`? – dirkgroten Feb 15 '19 at 14:48
  • @dirkgroten: Right. Well spotted. Thanks! Fixing it now. – djvg Feb 15 '19 at 14:54
  • Mmmm, if there is a onetoone relation with null=True your code will return True even if related.reporter is None? – Raydel Miranda Feb 15 '19 at 15:18
  • @RaydelMiranda: Let's say `SomeModel` has a `OneToOneField` pointing to `Reporter`. If this has `null=True` and there are no `SomeModel` objects at all, then `hasattr(reporter, 'somemodel_set')` will return `False`. – djvg Feb 15 '19 at 15:40
  • @RaydelMiranda: Does that answer your question? – djvg Feb 20 '19 at 09:16
  • I have to try, I think if you do `reporter.somemodel_set` you would get None if there is no `SomeModel` related. But if you can `reporter.somemodel_set` is because there is a `somemodel_set` attribute. – Raydel Miranda Feb 20 '19 at 12:32
  • @RaydelMiranda: No, it is a `OneToOneField`, so `reporter.somemodel_set` will raise an exception if there is no associated `SomeModel` object. There is an example of this specific behavior in the [docs for one-to-one relations](https://docs.djangoproject.com/en/2.1/topics/db/examples/one_to_one/#one-to-one-relationships) (search for `ObjectDoesNotExist`). They explicitly suggest using `hasattr`. – djvg Feb 20 '19 at 12:42
  • Yeah it will rise the excepion, but again I have to try, when you do `reporter.somemodel_set` and you get an `ObjectDoesNotExist` exeption instead of an "attribute not found" error that tell me the attribute is there. In fact ir also tell me that attribute is some kind of descriptor. – Raydel Miranda Feb 20 '19 at 13:56
  • @RaydelMiranda: Have a look at django's own [one-to-one getter test](https://github.com/django/django/blob/2.1.7/tests/one_to_one/tests.py#L30), which uses `hasattr`. – djvg Feb 21 '19 at 10:12
  • @RaydelMiranda: The `RelatedObjectDoesNotExist` is defined on the `RevereseOneToOneDescriptor` class, and has both `AttributeError` and `ObjectDoesNotExist` as its bases, as you can see [in the source](https://github.com/django/django/blob/2.1.7/django/db/models/fields/related_descriptors.py#L340). – djvg Feb 21 '19 at 10:26