I want to trigger some behaviour whenever a many-to-many relationship changes, but I'm unsure what signal setup is best for capturing changes to that relationship that come about due to the deletion of one side of the relationship. m2m_changed
does not seem to fire in this case, nor do the regular post_save
and post_delete
signals seem to be applicable to the through
model?
My current solution is to listen to pre_delete
on the models which compose either side of the relationship and then clear the relationship within that signal in order to trigger a m2m_changed
. I'm surprised that I have to do this though, and feel I've got something wrong.
What am I missing here? And if I am not missing anything why is this necessary (ie why is no such signal fired by default)?
Code example:
class ResearchField(models.Model):
name = models.CharField(unique=True, max_length=200)
class Researcher(models.Model):
name = models.CharField(max_length=200)
research_fields = models.ManyToManyField(ResearchField, blank=True)
@receiver(m2m_changed, sender=Researcher.research_fields.through)
def research_fields_changed(sender, instance, action, **kwargs):
# need to do something important here
print('m2m_changed', action)
volcanology = ResearchField.objects.create(name='Volcanology')
researcher = Researcher.objects.create(name='A. Volcanologist')
researcher.research_fields.add(volcanology)
>>> m2m_changed pre_add
>>> m2m_changed post_add
This m2m_changed
signal fires as expected when the relationship is removed from either side:
researcher.research_fields.remove(volcanology)
# or equally volcanology.researcher_set.remove(researcher)
>>> m2m_changed pre_remove
>>> m2m_changed post_remove
However no pre_remove
or post_remove
m2m_changed
signals fire if I simply delete one side of the relationship, despite the Django delete
output indicating that an instance of the through
model was removed:
# with the relationship intact
volcanology.delete()
>>> (2, {'ResearchField': 1, 'Researcher_research_fields': 1})
At this point I tried:
@receiver(post_delete, sender=Researcher.research_fields.through)
def through_model_deleted(sender, instance, **kwargs):
print('through model deleted')
But this never fires?
As a result my current solution is to have:
@receiver(pre_delete, sender=ResearchField)
def research_field_deleted(sender, instance, **kwargs):
instance.researcher_set.clear()
To custom enforce that when Researcher.research_fields.through
objects are being deleted via a cascade from a deleted ResearchField
the m2m_changed
signal will fire after all. However as I said at the top it feels like I've missed something that this is necessary?