Search code examples
djangodjango-modelsmany-to-manydjango-signals

How to handle a post-save signal for an M2M relationship with model inheritance in Django?


The question title is quite the sentence, but hopefully the code below will clear it up:

class Foo(models.Model):
    ...


class AbstractParent(models.Model):
    foos = models.ManyToManyField(
        Foo,
        related_name='%(app_label)s_%(class)s_related'
    )

    def bar(self):
        ...

    class Meta:
        abstract = True


class ChildOne(AbstractParent):
    ...


class ChildTwo(AbstractParent):
    ...

Lets say that my app's label is 'myapp'.

Basically, the base class of the ChildOne and ChildTwo has a M2M to the class Foo. What I want to do is this: whenever an object of the Foo class is saved, I want to call the bar() method of all the objects of ClassOne and ClassTwo which has a relation to the Foo object through the foos field. To do this, I tried writing a simple signal:

from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=Foo)
def call_bar_for_all_related_objects_to_foo(sender, instance, **kwargs):
    # Do the thing

At this point, Im kinda lost. How do I query all the children classes of the AbstractParent class and call their bar() methods whenever there is a relation to the instance in the signal? Ideally, I want to query my database only once, and in one query, I want to get all the objects of ChildOne and ChildTwo related to the instance in the signal. Please note, in my actual models, there are more than two child classes of the AbstractParent, so an answer that keeps that in mind is appreciated. Thanks for any help.


Solution

  • Well, it doesn't satisfy your single query requirement, but here's a way to at least get the job done with a query per child class:

    def call_bar_for_all_related_objects_to_foo(sender, instance, **kwargs):
        for field in instance._meta.get_fields():
            if not field.related_model:
                continue
            if not issubclass(field.related_model, AbstractParent):
                continue
            for related_object in getattr(instance, field.related_name).all():
                related_object.bar()
    

    Single query update

    I don't think this can be done in a single query in a general way like this. The only way I know to get whole related django objects out of a single query is via select_related, which doesn't work with ManyToMany relationships.

    If a single query is important, implementation will probably require more specific details on .bar(), which would likely need to be refactored into a class method that works on the results of a .values() call or something.